diff --git a/3rdp/build/Common.gmake b/3rdp/build/Common.gmake
index c74fd266892d39817fadbee58bbc88a21382b338..ce5b94aec47dd03d7030e80e3a39cd808587bdbf 100644
--- a/3rdp/build/Common.gmake
+++ b/3rdp/build/Common.gmake
@@ -138,3 +138,9 @@ ifdef CRYPTLIBDIR
  CRYPT_LDFLAGS += -L$(CRYPTLIBDIR)
 endif
 
+####################
+# libarchive stuff #
+####################
+ifeq ($(os),win32)
+ LDFLAGS += -L$(3RDP_ROOT)/win32.release/libarchive/bin
+endif
diff --git a/3rdp/win32.release/libarchive/bin/archive.dll b/3rdp/win32.release/libarchive/bin/archive.dll
new file mode 100644
index 0000000000000000000000000000000000000000..1650fba0460c4ddd531c41e41124f4f04e1a1129
Binary files /dev/null and b/3rdp/win32.release/libarchive/bin/archive.dll differ
diff --git a/3rdp/win32.release/libarchive/bin/archive.lib b/3rdp/win32.release/libarchive/bin/archive.lib
new file mode 100644
index 0000000000000000000000000000000000000000..88c79c17d4e463be925cef6cf62c9eaf678b7d5a
Binary files /dev/null and b/3rdp/win32.release/libarchive/bin/archive.lib differ
diff --git a/3rdp/win32.release/libarchive/include/archive.h b/3rdp/win32.release/libarchive/include/archive.h
new file mode 100644
index 0000000000000000000000000000000000000000..52f4d782953b4bc893cba2f7b889d7190d6b93a4
--- /dev/null
+++ b/3rdp/win32.release/libarchive/include/archive.h
@@ -0,0 +1,1204 @@
+/*-
+ * Copyright (c) 2003-2010 Tim Kientzle
+ * 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.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS 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 THE AUTHOR(S) 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.
+ *
+ * $FreeBSD: src/lib/libarchive/archive.h.in,v 1.50 2008/05/26 17:00:22 kientzle Exp $
+ */
+
+#ifndef ARCHIVE_H_INCLUDED
+#define	ARCHIVE_H_INCLUDED
+
+/*
+ * The version number is expressed as a single integer that makes it
+ * easy to compare versions at build time: for version a.b.c, the
+ * version number is printf("%d%03d%03d",a,b,c).  For example, if you
+ * know your application requires version 2.12.108 or later, you can
+ * assert that ARCHIVE_VERSION_NUMBER >= 2012108.
+ */
+/* Note: Compiler will complain if this does not match archive_entry.h! */
+#define	ARCHIVE_VERSION_NUMBER 3005001
+
+#include <sys/stat.h>
+#include <stddef.h>  /* for wchar_t */
+#include <stdio.h> /* For FILE * */
+#include <time.h> /* For time_t */
+
+/*
+ * Note: archive.h is for use outside of libarchive; the configuration
+ * headers (config.h, archive_platform.h, etc.) are purely internal.
+ * Do NOT use HAVE_XXX configuration macros to control the behavior of
+ * this header!  If you must conditionalize, use predefined compiler and/or
+ * platform macros.
+ */
+#if defined(__BORLANDC__) && __BORLANDC__ >= 0x560
+# include <stdint.h>
+#elif !defined(__WATCOMC__) && !defined(_MSC_VER) && !defined(__INTERIX) && !defined(__BORLANDC__) && !defined(_SCO_DS) && !defined(__osf__) && !defined(__CLANG_INTTYPES_H)
+# include <inttypes.h>
+#endif
+
+/* Get appropriate definitions of 64-bit integer */
+#if !defined(__LA_INT64_T_DEFINED)
+/* Older code relied on the __LA_INT64_T macro; after 4.0 we'll switch to the typedef exclusively. */
+# if ARCHIVE_VERSION_NUMBER < 4000000
+#define __LA_INT64_T la_int64_t
+# endif
+#define __LA_INT64_T_DEFINED
+# if defined(_WIN32) && !defined(__CYGWIN__) && !defined(__WATCOMC__)
+typedef __int64 la_int64_t;
+# else
+# include <unistd.h>  /* ssize_t */
+#  if defined(_SCO_DS) || defined(__osf__)
+typedef long long la_int64_t;
+#  else
+typedef int64_t la_int64_t;
+#  endif
+# endif
+#endif
+
+/* The la_ssize_t should match the type used in 'struct stat' */
+#if !defined(__LA_SSIZE_T_DEFINED)
+/* Older code relied on the __LA_SSIZE_T macro; after 4.0 we'll switch to the typedef exclusively. */
+# if ARCHIVE_VERSION_NUMBER < 4000000
+#define __LA_SSIZE_T la_ssize_t
+# endif
+#define __LA_SSIZE_T_DEFINED
+# if defined(_WIN32) && !defined(__CYGWIN__) && !defined(__WATCOMC__)
+#  if defined(_SSIZE_T_DEFINED) || defined(_SSIZE_T_)
+typedef ssize_t la_ssize_t;
+#  elif defined(_WIN64)
+typedef __int64 la_ssize_t;
+#  else
+typedef long la_ssize_t;
+#  endif
+# else
+# include <unistd.h>  /* ssize_t */
+typedef ssize_t la_ssize_t;
+# endif
+#endif
+
+/* Large file support for Android */
+#ifdef __ANDROID__
+#include "android_lf.h"
+#endif
+
+/*
+ * On Windows, define LIBARCHIVE_STATIC if you're building or using a
+ * .lib.  The default here assumes you're building a DLL.  Only
+ * libarchive source should ever define __LIBARCHIVE_BUILD.
+ */
+#if ((defined __WIN32__) || (defined _WIN32) || defined(__CYGWIN__)) && (!defined LIBARCHIVE_STATIC)
+# ifdef __LIBARCHIVE_BUILD
+#  ifdef __GNUC__
+#   define __LA_DECL	__attribute__((dllexport)) extern
+#  else
+#   define __LA_DECL	__declspec(dllexport)
+#  endif
+# else
+#  ifdef __GNUC__
+#   define __LA_DECL
+#  else
+#   define __LA_DECL	__declspec(dllimport)
+#  endif
+# endif
+#else
+/* Static libraries or non-Windows needs no special declaration. */
+# define __LA_DECL
+#endif
+
+#if defined(__GNUC__) && __GNUC__ >= 3 && !defined(__MINGW32__)
+#define	__LA_PRINTF(fmtarg, firstvararg) \
+	__attribute__((__format__ (__printf__, fmtarg, firstvararg)))
+#else
+#define	__LA_PRINTF(fmtarg, firstvararg)	/* nothing */
+#endif
+
+#if defined(__GNUC__) && __GNUC__ >= 3 && __GNUC_MINOR__ >= 1
+# define __LA_DEPRECATED __attribute__((deprecated))
+#else
+# define __LA_DEPRECATED
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*
+ * The version number is provided as both a macro and a function.
+ * The macro identifies the installed header; the function identifies
+ * the library version (which may not be the same if you're using a
+ * dynamically-linked version of the library).  Of course, if the
+ * header and library are very different, you should expect some
+ * strangeness.  Don't do that.
+ */
+__LA_DECL int		archive_version_number(void);
+
+/*
+ * Textual name/version of the library, useful for version displays.
+ */
+#define	ARCHIVE_VERSION_ONLY_STRING "3.5.1"
+#define	ARCHIVE_VERSION_STRING "libarchive " ARCHIVE_VERSION_ONLY_STRING
+__LA_DECL const char *	archive_version_string(void);
+
+/*
+ * Detailed textual name/version of the library and its dependencies.
+ * This has the form:
+ *    "libarchive x.y.z zlib/a.b.c liblzma/d.e.f ... etc ..."
+ * the list of libraries described here will vary depending on how
+ * libarchive was compiled.
+ */
+__LA_DECL const char *	archive_version_details(void);
+
+/*
+ * Returns NULL if libarchive was compiled without the associated library.
+ * Otherwise, returns the version number that libarchive was compiled
+ * against.
+ */
+__LA_DECL const char *  archive_zlib_version(void);
+__LA_DECL const char *  archive_liblzma_version(void);
+__LA_DECL const char *  archive_bzlib_version(void);
+__LA_DECL const char *  archive_liblz4_version(void);
+__LA_DECL const char *  archive_libzstd_version(void);
+
+/* Declare our basic types. */
+struct archive;
+struct archive_entry;
+
+/*
+ * Error codes: Use archive_errno() and archive_error_string()
+ * to retrieve details.  Unless specified otherwise, all functions
+ * that return 'int' use these codes.
+ */
+#define	ARCHIVE_EOF	  1	/* Found end of archive. */
+#define	ARCHIVE_OK	  0	/* Operation was successful. */
+#define	ARCHIVE_RETRY	(-10)	/* Retry might succeed. */
+#define	ARCHIVE_WARN	(-20)	/* Partial success. */
+/* For example, if write_header "fails", then you can't push data. */
+#define	ARCHIVE_FAILED	(-25)	/* Current operation cannot complete. */
+/* But if write_header is "fatal," then this archive is dead and useless. */
+#define	ARCHIVE_FATAL	(-30)	/* No more operations are possible. */
+
+/*
+ * As far as possible, archive_errno returns standard platform errno codes.
+ * Of course, the details vary by platform, so the actual definitions
+ * here are stored in "archive_platform.h".  The symbols are listed here
+ * for reference; as a rule, clients should not need to know the exact
+ * platform-dependent error code.
+ */
+/* Unrecognized or invalid file format. */
+/* #define	ARCHIVE_ERRNO_FILE_FORMAT */
+/* Illegal usage of the library. */
+/* #define	ARCHIVE_ERRNO_PROGRAMMER_ERROR */
+/* Unknown or unclassified error. */
+/* #define	ARCHIVE_ERRNO_MISC */
+
+/*
+ * Callbacks are invoked to automatically read/skip/write/open/close the
+ * archive. You can provide your own for complex tasks (like breaking
+ * archives across multiple tapes) or use standard ones built into the
+ * library.
+ */
+
+/* Returns pointer and size of next block of data from archive. */
+typedef la_ssize_t	archive_read_callback(struct archive *,
+			    void *_client_data, const void **_buffer);
+
+/* Skips at most request bytes from archive and returns the skipped amount.
+ * This may skip fewer bytes than requested; it may even skip zero bytes.
+ * If you do skip fewer bytes than requested, libarchive will invoke your
+ * read callback and discard data as necessary to make up the full skip.
+ */
+typedef la_int64_t	archive_skip_callback(struct archive *,
+			    void *_client_data, la_int64_t request);
+
+/* Seeks to specified location in the file and returns the position.
+ * Whence values are SEEK_SET, SEEK_CUR, SEEK_END from stdio.h.
+ * Return ARCHIVE_FATAL if the seek fails for any reason.
+ */
+typedef la_int64_t	archive_seek_callback(struct archive *,
+    void *_client_data, la_int64_t offset, int whence);
+
+/* Returns size actually written, zero on EOF, -1 on error. */
+typedef la_ssize_t	archive_write_callback(struct archive *,
+			    void *_client_data,
+			    const void *_buffer, size_t _length);
+
+typedef int	archive_open_callback(struct archive *, void *_client_data);
+
+typedef int	archive_close_callback(struct archive *, void *_client_data);
+
+typedef int	archive_free_callback(struct archive *, void *_client_data);
+
+/* Switches from one client data object to the next/prev client data object.
+ * This is useful for reading from different data blocks such as a set of files
+ * that make up one large file.
+ */
+typedef int archive_switch_callback(struct archive *, void *_client_data1,
+			    void *_client_data2);
+
+/*
+ * Returns a passphrase used for encryption or decryption, NULL on nothing
+ * to do and give it up.
+ */
+typedef const char *archive_passphrase_callback(struct archive *,
+			    void *_client_data);
+
+/*
+ * Codes to identify various stream filters.
+ */
+#define	ARCHIVE_FILTER_NONE	0
+#define	ARCHIVE_FILTER_GZIP	1
+#define	ARCHIVE_FILTER_BZIP2	2
+#define	ARCHIVE_FILTER_COMPRESS	3
+#define	ARCHIVE_FILTER_PROGRAM	4
+#define	ARCHIVE_FILTER_LZMA	5
+#define	ARCHIVE_FILTER_XZ	6
+#define	ARCHIVE_FILTER_UU	7
+#define	ARCHIVE_FILTER_RPM	8
+#define	ARCHIVE_FILTER_LZIP	9
+#define	ARCHIVE_FILTER_LRZIP	10
+#define	ARCHIVE_FILTER_LZOP	11
+#define	ARCHIVE_FILTER_GRZIP	12
+#define	ARCHIVE_FILTER_LZ4	13
+#define	ARCHIVE_FILTER_ZSTD	14
+
+#if ARCHIVE_VERSION_NUMBER < 4000000
+#define	ARCHIVE_COMPRESSION_NONE	ARCHIVE_FILTER_NONE
+#define	ARCHIVE_COMPRESSION_GZIP	ARCHIVE_FILTER_GZIP
+#define	ARCHIVE_COMPRESSION_BZIP2	ARCHIVE_FILTER_BZIP2
+#define	ARCHIVE_COMPRESSION_COMPRESS	ARCHIVE_FILTER_COMPRESS
+#define	ARCHIVE_COMPRESSION_PROGRAM	ARCHIVE_FILTER_PROGRAM
+#define	ARCHIVE_COMPRESSION_LZMA	ARCHIVE_FILTER_LZMA
+#define	ARCHIVE_COMPRESSION_XZ		ARCHIVE_FILTER_XZ
+#define	ARCHIVE_COMPRESSION_UU		ARCHIVE_FILTER_UU
+#define	ARCHIVE_COMPRESSION_RPM		ARCHIVE_FILTER_RPM
+#define	ARCHIVE_COMPRESSION_LZIP	ARCHIVE_FILTER_LZIP
+#define	ARCHIVE_COMPRESSION_LRZIP	ARCHIVE_FILTER_LRZIP
+#endif
+
+/*
+ * Codes returned by archive_format.
+ *
+ * Top 16 bits identifies the format family (e.g., "tar"); lower
+ * 16 bits indicate the variant.  This is updated by read_next_header.
+ * Note that the lower 16 bits will often vary from entry to entry.
+ * In some cases, this variation occurs as libarchive learns more about
+ * the archive (for example, later entries might utilize extensions that
+ * weren't necessary earlier in the archive; in this case, libarchive
+ * will change the format code to indicate the extended format that
+ * was used).  In other cases, it's because different tools have
+ * modified the archive and so different parts of the archive
+ * actually have slightly different formats.  (Both tar and cpio store
+ * format codes in each entry, so it is quite possible for each
+ * entry to be in a different format.)
+ */
+#define	ARCHIVE_FORMAT_BASE_MASK		0xff0000
+#define	ARCHIVE_FORMAT_CPIO			0x10000
+#define	ARCHIVE_FORMAT_CPIO_POSIX		(ARCHIVE_FORMAT_CPIO | 1)
+#define	ARCHIVE_FORMAT_CPIO_BIN_LE		(ARCHIVE_FORMAT_CPIO | 2)
+#define	ARCHIVE_FORMAT_CPIO_BIN_BE		(ARCHIVE_FORMAT_CPIO | 3)
+#define	ARCHIVE_FORMAT_CPIO_SVR4_NOCRC		(ARCHIVE_FORMAT_CPIO | 4)
+#define	ARCHIVE_FORMAT_CPIO_SVR4_CRC		(ARCHIVE_FORMAT_CPIO | 5)
+#define	ARCHIVE_FORMAT_CPIO_AFIO_LARGE		(ARCHIVE_FORMAT_CPIO | 6)
+#define	ARCHIVE_FORMAT_SHAR			0x20000
+#define	ARCHIVE_FORMAT_SHAR_BASE		(ARCHIVE_FORMAT_SHAR | 1)
+#define	ARCHIVE_FORMAT_SHAR_DUMP		(ARCHIVE_FORMAT_SHAR | 2)
+#define	ARCHIVE_FORMAT_TAR			0x30000
+#define	ARCHIVE_FORMAT_TAR_USTAR		(ARCHIVE_FORMAT_TAR | 1)
+#define	ARCHIVE_FORMAT_TAR_PAX_INTERCHANGE	(ARCHIVE_FORMAT_TAR | 2)
+#define	ARCHIVE_FORMAT_TAR_PAX_RESTRICTED	(ARCHIVE_FORMAT_TAR | 3)
+#define	ARCHIVE_FORMAT_TAR_GNUTAR		(ARCHIVE_FORMAT_TAR | 4)
+#define	ARCHIVE_FORMAT_ISO9660			0x40000
+#define	ARCHIVE_FORMAT_ISO9660_ROCKRIDGE	(ARCHIVE_FORMAT_ISO9660 | 1)
+#define	ARCHIVE_FORMAT_ZIP			0x50000
+#define	ARCHIVE_FORMAT_EMPTY			0x60000
+#define	ARCHIVE_FORMAT_AR			0x70000
+#define	ARCHIVE_FORMAT_AR_GNU			(ARCHIVE_FORMAT_AR | 1)
+#define	ARCHIVE_FORMAT_AR_BSD			(ARCHIVE_FORMAT_AR | 2)
+#define	ARCHIVE_FORMAT_MTREE			0x80000
+#define	ARCHIVE_FORMAT_RAW			0x90000
+#define	ARCHIVE_FORMAT_XAR			0xA0000
+#define	ARCHIVE_FORMAT_LHA			0xB0000
+#define	ARCHIVE_FORMAT_CAB			0xC0000
+#define	ARCHIVE_FORMAT_RAR			0xD0000
+#define	ARCHIVE_FORMAT_7ZIP			0xE0000
+#define	ARCHIVE_FORMAT_WARC			0xF0000
+#define	ARCHIVE_FORMAT_RAR_V5			0x100000
+
+/*
+ * Codes returned by archive_read_format_capabilities().
+ *
+ * This list can be extended with values between 0 and 0xffff.
+ * The original purpose of this list was to let different archive
+ * format readers expose their general capabilities in terms of
+ * encryption.
+ */
+#define ARCHIVE_READ_FORMAT_CAPS_NONE (0) /* no special capabilities */
+#define ARCHIVE_READ_FORMAT_CAPS_ENCRYPT_DATA (1<<0)  /* reader can detect encrypted data */
+#define ARCHIVE_READ_FORMAT_CAPS_ENCRYPT_METADATA (1<<1)  /* reader can detect encryptable metadata (pathname, mtime, etc.) */
+
+/*
+ * Codes returned by archive_read_has_encrypted_entries().
+ *
+ * In case the archive does not support encryption detection at all
+ * ARCHIVE_READ_FORMAT_ENCRYPTION_UNSUPPORTED is returned. If the reader
+ * for some other reason (e.g. not enough bytes read) cannot say if
+ * there are encrypted entries, ARCHIVE_READ_FORMAT_ENCRYPTION_DONT_KNOW
+ * is returned.
+ */
+#define ARCHIVE_READ_FORMAT_ENCRYPTION_UNSUPPORTED -2
+#define ARCHIVE_READ_FORMAT_ENCRYPTION_DONT_KNOW -1
+
+/*-
+ * Basic outline for reading an archive:
+ *   1) Ask archive_read_new for an archive reader object.
+ *   2) Update any global properties as appropriate.
+ *      In particular, you'll certainly want to call appropriate
+ *      archive_read_support_XXX functions.
+ *   3) Call archive_read_open_XXX to open the archive
+ *   4) Repeatedly call archive_read_next_header to get information about
+ *      successive archive entries.  Call archive_read_data to extract
+ *      data for entries of interest.
+ *   5) Call archive_read_free to end processing.
+ */
+__LA_DECL struct archive	*archive_read_new(void);
+
+/*
+ * The archive_read_support_XXX calls enable auto-detect for this
+ * archive handle.  They also link in the necessary support code.
+ * For example, if you don't want bzlib linked in, don't invoke
+ * support_compression_bzip2().  The "all" functions provide the
+ * obvious shorthand.
+ */
+
+#if ARCHIVE_VERSION_NUMBER < 4000000
+__LA_DECL int archive_read_support_compression_all(struct archive *)
+		__LA_DEPRECATED;
+__LA_DECL int archive_read_support_compression_bzip2(struct archive *)
+		__LA_DEPRECATED;
+__LA_DECL int archive_read_support_compression_compress(struct archive *)
+		__LA_DEPRECATED;
+__LA_DECL int archive_read_support_compression_gzip(struct archive *)
+		__LA_DEPRECATED;
+__LA_DECL int archive_read_support_compression_lzip(struct archive *)
+		__LA_DEPRECATED;
+__LA_DECL int archive_read_support_compression_lzma(struct archive *)
+		__LA_DEPRECATED;
+__LA_DECL int archive_read_support_compression_none(struct archive *)
+		__LA_DEPRECATED;
+__LA_DECL int archive_read_support_compression_program(struct archive *,
+		     const char *command) __LA_DEPRECATED;
+__LA_DECL int archive_read_support_compression_program_signature
+		(struct archive *, const char *,
+		 const void * /* match */, size_t) __LA_DEPRECATED;
+
+__LA_DECL int archive_read_support_compression_rpm(struct archive *)
+		__LA_DEPRECATED;
+__LA_DECL int archive_read_support_compression_uu(struct archive *)
+		__LA_DEPRECATED;
+__LA_DECL int archive_read_support_compression_xz(struct archive *)
+		__LA_DEPRECATED;
+#endif
+
+__LA_DECL int archive_read_support_filter_all(struct archive *);
+__LA_DECL int archive_read_support_filter_by_code(struct archive *, int);
+__LA_DECL int archive_read_support_filter_bzip2(struct archive *);
+__LA_DECL int archive_read_support_filter_compress(struct archive *);
+__LA_DECL int archive_read_support_filter_gzip(struct archive *);
+__LA_DECL int archive_read_support_filter_grzip(struct archive *);
+__LA_DECL int archive_read_support_filter_lrzip(struct archive *);
+__LA_DECL int archive_read_support_filter_lz4(struct archive *);
+__LA_DECL int archive_read_support_filter_lzip(struct archive *);
+__LA_DECL int archive_read_support_filter_lzma(struct archive *);
+__LA_DECL int archive_read_support_filter_lzop(struct archive *);
+__LA_DECL int archive_read_support_filter_none(struct archive *);
+__LA_DECL int archive_read_support_filter_program(struct archive *,
+		     const char *command);
+__LA_DECL int archive_read_support_filter_program_signature
+		(struct archive *, const char * /* cmd */,
+				    const void * /* match */, size_t);
+__LA_DECL int archive_read_support_filter_rpm(struct archive *);
+__LA_DECL int archive_read_support_filter_uu(struct archive *);
+__LA_DECL int archive_read_support_filter_xz(struct archive *);
+__LA_DECL int archive_read_support_filter_zstd(struct archive *);
+
+__LA_DECL int archive_read_support_format_7zip(struct archive *);
+__LA_DECL int archive_read_support_format_all(struct archive *);
+__LA_DECL int archive_read_support_format_ar(struct archive *);
+__LA_DECL int archive_read_support_format_by_code(struct archive *, int);
+__LA_DECL int archive_read_support_format_cab(struct archive *);
+__LA_DECL int archive_read_support_format_cpio(struct archive *);
+__LA_DECL int archive_read_support_format_empty(struct archive *);
+__LA_DECL int archive_read_support_format_gnutar(struct archive *);
+__LA_DECL int archive_read_support_format_iso9660(struct archive *);
+__LA_DECL int archive_read_support_format_lha(struct archive *);
+__LA_DECL int archive_read_support_format_mtree(struct archive *);
+__LA_DECL int archive_read_support_format_rar(struct archive *);
+__LA_DECL int archive_read_support_format_rar5(struct archive *);
+__LA_DECL int archive_read_support_format_raw(struct archive *);
+__LA_DECL int archive_read_support_format_tar(struct archive *);
+__LA_DECL int archive_read_support_format_warc(struct archive *);
+__LA_DECL int archive_read_support_format_xar(struct archive *);
+/* archive_read_support_format_zip() enables both streamable and seekable
+ * zip readers. */
+__LA_DECL int archive_read_support_format_zip(struct archive *);
+/* Reads Zip archives as stream from beginning to end.  Doesn't
+ * correctly handle SFX ZIP files or ZIP archives that have been modified
+ * in-place. */
+__LA_DECL int archive_read_support_format_zip_streamable(struct archive *);
+/* Reads starting from central directory; requires seekable input. */
+__LA_DECL int archive_read_support_format_zip_seekable(struct archive *);
+
+/* Functions to manually set the format and filters to be used. This is
+ * useful to bypass the bidding process when the format and filters to use
+ * is known in advance.
+ */
+__LA_DECL int archive_read_set_format(struct archive *, int);
+__LA_DECL int archive_read_append_filter(struct archive *, int);
+__LA_DECL int archive_read_append_filter_program(struct archive *,
+    const char *);
+__LA_DECL int archive_read_append_filter_program_signature
+    (struct archive *, const char *, const void * /* match */, size_t);
+
+/* Set various callbacks. */
+__LA_DECL int archive_read_set_open_callback(struct archive *,
+    archive_open_callback *);
+__LA_DECL int archive_read_set_read_callback(struct archive *,
+    archive_read_callback *);
+__LA_DECL int archive_read_set_seek_callback(struct archive *,
+    archive_seek_callback *);
+__LA_DECL int archive_read_set_skip_callback(struct archive *,
+    archive_skip_callback *);
+__LA_DECL int archive_read_set_close_callback(struct archive *,
+    archive_close_callback *);
+/* Callback used to switch between one data object to the next */
+__LA_DECL int archive_read_set_switch_callback(struct archive *,
+    archive_switch_callback *);
+
+/* This sets the first data object. */
+__LA_DECL int archive_read_set_callback_data(struct archive *, void *);
+/* This sets data object at specified index */
+__LA_DECL int archive_read_set_callback_data2(struct archive *, void *,
+    unsigned int);
+/* This adds a data object at the specified index. */
+__LA_DECL int archive_read_add_callback_data(struct archive *, void *,
+    unsigned int);
+/* This appends a data object to the end of list */
+__LA_DECL int archive_read_append_callback_data(struct archive *, void *);
+/* This prepends a data object to the beginning of list */
+__LA_DECL int archive_read_prepend_callback_data(struct archive *, void *);
+
+/* Opening freezes the callbacks. */
+__LA_DECL int archive_read_open1(struct archive *);
+
+/* Convenience wrappers around the above. */
+__LA_DECL int archive_read_open(struct archive *, void *_client_data,
+		     archive_open_callback *, archive_read_callback *,
+		     archive_close_callback *);
+__LA_DECL int archive_read_open2(struct archive *, void *_client_data,
+		     archive_open_callback *, archive_read_callback *,
+		     archive_skip_callback *, archive_close_callback *);
+
+/*
+ * A variety of shortcuts that invoke archive_read_open() with
+ * canned callbacks suitable for common situations.  The ones that
+ * accept a block size handle tape blocking correctly.
+ */
+/* Use this if you know the filename.  Note: NULL indicates stdin. */
+__LA_DECL int archive_read_open_filename(struct archive *,
+		     const char *_filename, size_t _block_size);
+/* Use this for reading multivolume files by filenames.
+ * NOTE: Must be NULL terminated. Sorting is NOT done. */
+__LA_DECL int archive_read_open_filenames(struct archive *,
+		     const char **_filenames, size_t _block_size);
+__LA_DECL int archive_read_open_filename_w(struct archive *,
+		     const wchar_t *_filename, size_t _block_size);
+/* archive_read_open_file() is a deprecated synonym for ..._open_filename(). */
+__LA_DECL int archive_read_open_file(struct archive *,
+		     const char *_filename, size_t _block_size) __LA_DEPRECATED;
+/* Read an archive that's stored in memory. */
+__LA_DECL int archive_read_open_memory(struct archive *,
+		     const void * buff, size_t size);
+/* A more involved version that is only used for internal testing. */
+__LA_DECL int archive_read_open_memory2(struct archive *a, const void *buff,
+		     size_t size, size_t read_size);
+/* Read an archive that's already open, using the file descriptor. */
+__LA_DECL int archive_read_open_fd(struct archive *, int _fd,
+		     size_t _block_size);
+/* Read an archive that's already open, using a FILE *. */
+/* Note: DO NOT use this with tape drives. */
+__LA_DECL int archive_read_open_FILE(struct archive *, FILE *_file);
+
+/* Parses and returns next entry header. */
+__LA_DECL int archive_read_next_header(struct archive *,
+		     struct archive_entry **);
+
+/* Parses and returns next entry header using the archive_entry passed in */
+__LA_DECL int archive_read_next_header2(struct archive *,
+		     struct archive_entry *);
+
+/*
+ * Retrieve the byte offset in UNCOMPRESSED data where last-read
+ * header started.
+ */
+__LA_DECL la_int64_t		 archive_read_header_position(struct archive *);
+
+/*
+ * Returns 1 if the archive contains at least one encrypted entry.
+ * If the archive format not support encryption at all
+ * ARCHIVE_READ_FORMAT_ENCRYPTION_UNSUPPORTED is returned.
+ * If for any other reason (e.g. not enough data read so far)
+ * we cannot say whether there are encrypted entries, then
+ * ARCHIVE_READ_FORMAT_ENCRYPTION_DONT_KNOW is returned.
+ * In general, this function will return values below zero when the
+ * reader is uncertain or totally incapable of encryption support.
+ * When this function returns 0 you can be sure that the reader
+ * supports encryption detection but no encrypted entries have
+ * been found yet.
+ *
+ * NOTE: If the metadata/header of an archive is also encrypted, you
+ * cannot rely on the number of encrypted entries. That is why this
+ * function does not return the number of encrypted entries but#
+ * just shows that there are some.
+ */
+__LA_DECL int	archive_read_has_encrypted_entries(struct archive *);
+
+/*
+ * Returns a bitmask of capabilities that are supported by the archive format reader.
+ * If the reader has no special capabilities, ARCHIVE_READ_FORMAT_CAPS_NONE is returned.
+ */
+__LA_DECL int		 archive_read_format_capabilities(struct archive *);
+
+/* Read data from the body of an entry.  Similar to read(2). */
+__LA_DECL la_ssize_t		 archive_read_data(struct archive *,
+				    void *, size_t);
+
+/* Seek within the body of an entry.  Similar to lseek(2). */
+__LA_DECL la_int64_t archive_seek_data(struct archive *, la_int64_t, int);
+
+/*
+ * A zero-copy version of archive_read_data that also exposes the file offset
+ * of each returned block.  Note that the client has no way to specify
+ * the desired size of the block.  The API does guarantee that offsets will
+ * be strictly increasing and that returned blocks will not overlap.
+ */
+__LA_DECL int archive_read_data_block(struct archive *a,
+		    const void **buff, size_t *size, la_int64_t *offset);
+
+/*-
+ * Some convenience functions that are built on archive_read_data:
+ *  'skip': skips entire entry
+ *  'into_buffer': writes data into memory buffer that you provide
+ *  'into_fd': writes data to specified filedes
+ */
+__LA_DECL int archive_read_data_skip(struct archive *);
+__LA_DECL int archive_read_data_into_fd(struct archive *, int fd);
+
+/*
+ * Set read options.
+ */
+/* Apply option to the format only. */
+__LA_DECL int archive_read_set_format_option(struct archive *_a,
+			    const char *m, const char *o,
+			    const char *v);
+/* Apply option to the filter only. */
+__LA_DECL int archive_read_set_filter_option(struct archive *_a,
+			    const char *m, const char *o,
+			    const char *v);
+/* Apply option to both the format and the filter. */
+__LA_DECL int archive_read_set_option(struct archive *_a,
+			    const char *m, const char *o,
+			    const char *v);
+/* Apply option string to both the format and the filter. */
+__LA_DECL int archive_read_set_options(struct archive *_a,
+			    const char *opts);
+
+/*
+ * Add a decryption passphrase.
+ */
+__LA_DECL int archive_read_add_passphrase(struct archive *, const char *);
+__LA_DECL int archive_read_set_passphrase_callback(struct archive *,
+			    void *client_data, archive_passphrase_callback *);
+
+
+/*-
+ * Convenience function to recreate the current entry (whose header
+ * has just been read) on disk.
+ *
+ * This does quite a bit more than just copy data to disk. It also:
+ *  - Creates intermediate directories as required.
+ *  - Manages directory permissions:  non-writable directories will
+ *    be initially created with write permission enabled; when the
+ *    archive is closed, dir permissions are edited to the values specified
+ *    in the archive.
+ *  - Checks hardlinks:  hardlinks will not be extracted unless the
+ *    linked-to file was also extracted within the same session. (TODO)
+ */
+
+/* The "flags" argument selects optional behavior, 'OR' the flags you want. */
+
+/* Default: Do not try to set owner/group. */
+#define	ARCHIVE_EXTRACT_OWNER			(0x0001)
+/* Default: Do obey umask, do not restore SUID/SGID/SVTX bits. */
+#define	ARCHIVE_EXTRACT_PERM			(0x0002)
+/* Default: Do not restore mtime/atime. */
+#define	ARCHIVE_EXTRACT_TIME			(0x0004)
+/* Default: Replace existing files. */
+#define	ARCHIVE_EXTRACT_NO_OVERWRITE 		(0x0008)
+/* Default: Try create first, unlink only if create fails with EEXIST. */
+#define	ARCHIVE_EXTRACT_UNLINK			(0x0010)
+/* Default: Do not restore ACLs. */
+#define	ARCHIVE_EXTRACT_ACL			(0x0020)
+/* Default: Do not restore fflags. */
+#define	ARCHIVE_EXTRACT_FFLAGS			(0x0040)
+/* Default: Do not restore xattrs. */
+#define	ARCHIVE_EXTRACT_XATTR 			(0x0080)
+/* Default: Do not try to guard against extracts redirected by symlinks. */
+/* Note: With ARCHIVE_EXTRACT_UNLINK, will remove any intermediate symlink. */
+#define	ARCHIVE_EXTRACT_SECURE_SYMLINKS		(0x0100)
+/* Default: Do not reject entries with '..' as path elements. */
+#define	ARCHIVE_EXTRACT_SECURE_NODOTDOT		(0x0200)
+/* Default: Create parent directories as needed. */
+#define	ARCHIVE_EXTRACT_NO_AUTODIR		(0x0400)
+/* Default: Overwrite files, even if one on disk is newer. */
+#define	ARCHIVE_EXTRACT_NO_OVERWRITE_NEWER	(0x0800)
+/* Detect blocks of 0 and write holes instead. */
+#define	ARCHIVE_EXTRACT_SPARSE			(0x1000)
+/* Default: Do not restore Mac extended metadata. */
+/* This has no effect except on Mac OS. */
+#define	ARCHIVE_EXTRACT_MAC_METADATA		(0x2000)
+/* Default: Use HFS+ compression if it was compressed. */
+/* This has no effect except on Mac OS v10.6 or later. */
+#define	ARCHIVE_EXTRACT_NO_HFS_COMPRESSION	(0x4000)
+/* Default: Do not use HFS+ compression if it was not compressed. */
+/* This has no effect except on Mac OS v10.6 or later. */
+#define	ARCHIVE_EXTRACT_HFS_COMPRESSION_FORCED	(0x8000)
+/* Default: Do not reject entries with absolute paths */
+#define ARCHIVE_EXTRACT_SECURE_NOABSOLUTEPATHS (0x10000)
+/* Default: Do not clear no-change flags when unlinking object */
+#define	ARCHIVE_EXTRACT_CLEAR_NOCHANGE_FFLAGS	(0x20000)
+/* Default: Do not extract atomically (using rename) */
+#define	ARCHIVE_EXTRACT_SAFE_WRITES		(0x40000)
+
+__LA_DECL int archive_read_extract(struct archive *, struct archive_entry *,
+		     int flags);
+__LA_DECL int archive_read_extract2(struct archive *, struct archive_entry *,
+		     struct archive * /* dest */);
+__LA_DECL void	 archive_read_extract_set_progress_callback(struct archive *,
+		     void (*_progress_func)(void *), void *_user_data);
+
+/* Record the dev/ino of a file that will not be written.  This is
+ * generally set to the dev/ino of the archive being read. */
+__LA_DECL void		archive_read_extract_set_skip_file(struct archive *,
+		     la_int64_t, la_int64_t);
+
+/* Close the file and release most resources. */
+__LA_DECL int		 archive_read_close(struct archive *);
+/* Release all resources and destroy the object. */
+/* Note that archive_read_free will call archive_read_close for you. */
+__LA_DECL int		 archive_read_free(struct archive *);
+#if ARCHIVE_VERSION_NUMBER < 4000000
+/* Synonym for archive_read_free() for backwards compatibility. */
+__LA_DECL int		 archive_read_finish(struct archive *) __LA_DEPRECATED;
+#endif
+
+/*-
+ * To create an archive:
+ *   1) Ask archive_write_new for an archive writer object.
+ *   2) Set any global properties.  In particular, you should set
+ *      the compression and format to use.
+ *   3) Call archive_write_open to open the file (most people
+ *       will use archive_write_open_file or archive_write_open_fd,
+ *       which provide convenient canned I/O callbacks for you).
+ *   4) For each entry:
+ *      - construct an appropriate struct archive_entry structure
+ *      - archive_write_header to write the header
+ *      - archive_write_data to write the entry data
+ *   5) archive_write_close to close the output
+ *   6) archive_write_free to cleanup the writer and release resources
+ */
+__LA_DECL struct archive	*archive_write_new(void);
+__LA_DECL int archive_write_set_bytes_per_block(struct archive *,
+		     int bytes_per_block);
+__LA_DECL int archive_write_get_bytes_per_block(struct archive *);
+/* XXX This is badly misnamed; suggestions appreciated. XXX */
+__LA_DECL int archive_write_set_bytes_in_last_block(struct archive *,
+		     int bytes_in_last_block);
+__LA_DECL int archive_write_get_bytes_in_last_block(struct archive *);
+
+/* The dev/ino of a file that won't be archived.  This is used
+ * to avoid recursively adding an archive to itself. */
+__LA_DECL int archive_write_set_skip_file(struct archive *,
+    la_int64_t, la_int64_t);
+
+#if ARCHIVE_VERSION_NUMBER < 4000000
+__LA_DECL int archive_write_set_compression_bzip2(struct archive *)
+		__LA_DEPRECATED;
+__LA_DECL int archive_write_set_compression_compress(struct archive *)
+		__LA_DEPRECATED;
+__LA_DECL int archive_write_set_compression_gzip(struct archive *)
+		__LA_DEPRECATED;
+__LA_DECL int archive_write_set_compression_lzip(struct archive *)
+		__LA_DEPRECATED;
+__LA_DECL int archive_write_set_compression_lzma(struct archive *)
+		__LA_DEPRECATED;
+__LA_DECL int archive_write_set_compression_none(struct archive *)
+		__LA_DEPRECATED;
+__LA_DECL int archive_write_set_compression_program(struct archive *,
+		     const char *cmd) __LA_DEPRECATED;
+__LA_DECL int archive_write_set_compression_xz(struct archive *)
+		__LA_DEPRECATED;
+#endif
+
+/* A convenience function to set the filter based on the code. */
+__LA_DECL int archive_write_add_filter(struct archive *, int filter_code);
+__LA_DECL int archive_write_add_filter_by_name(struct archive *,
+		     const char *name);
+__LA_DECL int archive_write_add_filter_b64encode(struct archive *);
+__LA_DECL int archive_write_add_filter_bzip2(struct archive *);
+__LA_DECL int archive_write_add_filter_compress(struct archive *);
+__LA_DECL int archive_write_add_filter_grzip(struct archive *);
+__LA_DECL int archive_write_add_filter_gzip(struct archive *);
+__LA_DECL int archive_write_add_filter_lrzip(struct archive *);
+__LA_DECL int archive_write_add_filter_lz4(struct archive *);
+__LA_DECL int archive_write_add_filter_lzip(struct archive *);
+__LA_DECL int archive_write_add_filter_lzma(struct archive *);
+__LA_DECL int archive_write_add_filter_lzop(struct archive *);
+__LA_DECL int archive_write_add_filter_none(struct archive *);
+__LA_DECL int archive_write_add_filter_program(struct archive *,
+		     const char *cmd);
+__LA_DECL int archive_write_add_filter_uuencode(struct archive *);
+__LA_DECL int archive_write_add_filter_xz(struct archive *);
+__LA_DECL int archive_write_add_filter_zstd(struct archive *);
+
+
+/* A convenience function to set the format based on the code or name. */
+__LA_DECL int archive_write_set_format(struct archive *, int format_code);
+__LA_DECL int archive_write_set_format_by_name(struct archive *,
+		     const char *name);
+/* To minimize link pollution, use one or more of the following. */
+__LA_DECL int archive_write_set_format_7zip(struct archive *);
+__LA_DECL int archive_write_set_format_ar_bsd(struct archive *);
+__LA_DECL int archive_write_set_format_ar_svr4(struct archive *);
+__LA_DECL int archive_write_set_format_cpio(struct archive *);
+__LA_DECL int archive_write_set_format_cpio_newc(struct archive *);
+__LA_DECL int archive_write_set_format_gnutar(struct archive *);
+__LA_DECL int archive_write_set_format_iso9660(struct archive *);
+__LA_DECL int archive_write_set_format_mtree(struct archive *);
+__LA_DECL int archive_write_set_format_mtree_classic(struct archive *);
+/* TODO: int archive_write_set_format_old_tar(struct archive *); */
+__LA_DECL int archive_write_set_format_pax(struct archive *);
+__LA_DECL int archive_write_set_format_pax_restricted(struct archive *);
+__LA_DECL int archive_write_set_format_raw(struct archive *);
+__LA_DECL int archive_write_set_format_shar(struct archive *);
+__LA_DECL int archive_write_set_format_shar_dump(struct archive *);
+__LA_DECL int archive_write_set_format_ustar(struct archive *);
+__LA_DECL int archive_write_set_format_v7tar(struct archive *);
+__LA_DECL int archive_write_set_format_warc(struct archive *);
+__LA_DECL int archive_write_set_format_xar(struct archive *);
+__LA_DECL int archive_write_set_format_zip(struct archive *);
+__LA_DECL int archive_write_set_format_filter_by_ext(struct archive *a, const char *filename);
+__LA_DECL int archive_write_set_format_filter_by_ext_def(struct archive *a, const char *filename, const char * def_ext);
+__LA_DECL int archive_write_zip_set_compression_deflate(struct archive *);
+__LA_DECL int archive_write_zip_set_compression_store(struct archive *);
+/* Deprecated; use archive_write_open2 instead */
+__LA_DECL int archive_write_open(struct archive *, void *,
+		     archive_open_callback *, archive_write_callback *,
+		     archive_close_callback *);
+__LA_DECL int archive_write_open2(struct archive *, void *,
+		     archive_open_callback *, archive_write_callback *,
+		     archive_close_callback *, archive_free_callback *);
+__LA_DECL int archive_write_open_fd(struct archive *, int _fd);
+__LA_DECL int archive_write_open_filename(struct archive *, const char *_file);
+__LA_DECL int archive_write_open_filename_w(struct archive *,
+		     const wchar_t *_file);
+/* A deprecated synonym for archive_write_open_filename() */
+__LA_DECL int archive_write_open_file(struct archive *, const char *_file)
+		__LA_DEPRECATED;
+__LA_DECL int archive_write_open_FILE(struct archive *, FILE *);
+/* _buffSize is the size of the buffer, _used refers to a variable that
+ * will be updated after each write into the buffer. */
+__LA_DECL int archive_write_open_memory(struct archive *,
+			void *_buffer, size_t _buffSize, size_t *_used);
+
+/*
+ * Note that the library will truncate writes beyond the size provided
+ * to archive_write_header or pad if the provided data is short.
+ */
+__LA_DECL int archive_write_header(struct archive *,
+		     struct archive_entry *);
+__LA_DECL la_ssize_t	archive_write_data(struct archive *,
+			    const void *, size_t);
+
+/* This interface is currently only available for archive_write_disk handles.  */
+__LA_DECL la_ssize_t	 archive_write_data_block(struct archive *,
+				    const void *, size_t, la_int64_t);
+
+__LA_DECL int		 archive_write_finish_entry(struct archive *);
+__LA_DECL int		 archive_write_close(struct archive *);
+/* Marks the archive as FATAL so that a subsequent free() operation
+ * won't try to close() cleanly.  Provides a fast abort capability
+ * when the client discovers that things have gone wrong. */
+__LA_DECL int            archive_write_fail(struct archive *);
+/* This can fail if the archive wasn't already closed, in which case
+ * archive_write_free() will implicitly call archive_write_close(). */
+__LA_DECL int		 archive_write_free(struct archive *);
+#if ARCHIVE_VERSION_NUMBER < 4000000
+/* Synonym for archive_write_free() for backwards compatibility. */
+__LA_DECL int		 archive_write_finish(struct archive *) __LA_DEPRECATED;
+#endif
+
+/*
+ * Set write options.
+ */
+/* Apply option to the format only. */
+__LA_DECL int archive_write_set_format_option(struct archive *_a,
+			    const char *m, const char *o,
+			    const char *v);
+/* Apply option to the filter only. */
+__LA_DECL int archive_write_set_filter_option(struct archive *_a,
+			    const char *m, const char *o,
+			    const char *v);
+/* Apply option to both the format and the filter. */
+__LA_DECL int archive_write_set_option(struct archive *_a,
+			    const char *m, const char *o,
+			    const char *v);
+/* Apply option string to both the format and the filter. */
+__LA_DECL int archive_write_set_options(struct archive *_a,
+			    const char *opts);
+
+/*
+ * Set a encryption passphrase.
+ */
+__LA_DECL int archive_write_set_passphrase(struct archive *_a, const char *p);
+__LA_DECL int archive_write_set_passphrase_callback(struct archive *,
+			    void *client_data, archive_passphrase_callback *);
+
+/*-
+ * ARCHIVE_WRITE_DISK API
+ *
+ * To create objects on disk:
+ *   1) Ask archive_write_disk_new for a new archive_write_disk object.
+ *   2) Set any global properties.  In particular, you probably
+ *      want to set the options.
+ *   3) For each entry:
+ *      - construct an appropriate struct archive_entry structure
+ *      - archive_write_header to create the file/dir/etc on disk
+ *      - archive_write_data to write the entry data
+ *   4) archive_write_free to cleanup the writer and release resources
+ *
+ * In particular, you can use this in conjunction with archive_read()
+ * to pull entries out of an archive and create them on disk.
+ */
+__LA_DECL struct archive	*archive_write_disk_new(void);
+/* This file will not be overwritten. */
+__LA_DECL int archive_write_disk_set_skip_file(struct archive *,
+    la_int64_t, la_int64_t);
+/* Set flags to control how the next item gets created.
+ * This accepts a bitmask of ARCHIVE_EXTRACT_XXX flags defined above. */
+__LA_DECL int		 archive_write_disk_set_options(struct archive *,
+		     int flags);
+/*
+ * The lookup functions are given uname/uid (or gname/gid) pairs and
+ * return a uid (gid) suitable for this system.  These are used for
+ * restoring ownership and for setting ACLs.  The default functions
+ * are naive, they just return the uid/gid.  These are small, so reasonable
+ * for applications that don't need to preserve ownership; they
+ * are probably also appropriate for applications that are doing
+ * same-system backup and restore.
+ */
+/*
+ * The "standard" lookup functions use common system calls to lookup
+ * the uname/gname, falling back to the uid/gid if the names can't be
+ * found.  They cache lookups and are reasonably fast, but can be very
+ * large, so they are not used unless you ask for them.  In
+ * particular, these match the specifications of POSIX "pax" and old
+ * POSIX "tar".
+ */
+__LA_DECL int	 archive_write_disk_set_standard_lookup(struct archive *);
+/*
+ * If neither the default (naive) nor the standard (big) functions suit
+ * your needs, you can write your own and register them.  Be sure to
+ * include a cleanup function if you have allocated private data.
+ */
+__LA_DECL int archive_write_disk_set_group_lookup(struct archive *,
+    void * /* private_data */,
+    la_int64_t (*)(void *, const char *, la_int64_t),
+    void (* /* cleanup */)(void *));
+__LA_DECL int archive_write_disk_set_user_lookup(struct archive *,
+    void * /* private_data */,
+    la_int64_t (*)(void *, const char *, la_int64_t),
+    void (* /* cleanup */)(void *));
+__LA_DECL la_int64_t archive_write_disk_gid(struct archive *, const char *, la_int64_t);
+__LA_DECL la_int64_t archive_write_disk_uid(struct archive *, const char *, la_int64_t);
+
+/*
+ * ARCHIVE_READ_DISK API
+ *
+ * This is still evolving and somewhat experimental.
+ */
+__LA_DECL struct archive *archive_read_disk_new(void);
+/* The names for symlink modes here correspond to an old BSD
+ * command-line argument convention: -L, -P, -H */
+/* Follow all symlinks. */
+__LA_DECL int archive_read_disk_set_symlink_logical(struct archive *);
+/* Follow no symlinks. */
+__LA_DECL int archive_read_disk_set_symlink_physical(struct archive *);
+/* Follow symlink initially, then not. */
+__LA_DECL int archive_read_disk_set_symlink_hybrid(struct archive *);
+/* TODO: Handle Linux stat32/stat64 ugliness. <sigh> */
+__LA_DECL int archive_read_disk_entry_from_file(struct archive *,
+    struct archive_entry *, int /* fd */, const struct stat *);
+/* Look up gname for gid or uname for uid. */
+/* Default implementations are very, very stupid. */
+__LA_DECL const char *archive_read_disk_gname(struct archive *, la_int64_t);
+__LA_DECL const char *archive_read_disk_uname(struct archive *, la_int64_t);
+/* "Standard" implementation uses getpwuid_r, getgrgid_r and caches the
+ * results for performance. */
+__LA_DECL int	archive_read_disk_set_standard_lookup(struct archive *);
+/* You can install your own lookups if you like. */
+__LA_DECL int	archive_read_disk_set_gname_lookup(struct archive *,
+    void * /* private_data */,
+    const char *(* /* lookup_fn */)(void *, la_int64_t),
+    void (* /* cleanup_fn */)(void *));
+__LA_DECL int	archive_read_disk_set_uname_lookup(struct archive *,
+    void * /* private_data */,
+    const char *(* /* lookup_fn */)(void *, la_int64_t),
+    void (* /* cleanup_fn */)(void *));
+/* Start traversal. */
+__LA_DECL int	archive_read_disk_open(struct archive *, const char *);
+__LA_DECL int	archive_read_disk_open_w(struct archive *, const wchar_t *);
+/*
+ * Request that current entry be visited.  If you invoke it on every
+ * directory, you'll get a physical traversal.  This is ignored if the
+ * current entry isn't a directory or a link to a directory.  So, if
+ * you invoke this on every returned path, you'll get a full logical
+ * traversal.
+ */
+__LA_DECL int	archive_read_disk_descend(struct archive *);
+__LA_DECL int	archive_read_disk_can_descend(struct archive *);
+__LA_DECL int	archive_read_disk_current_filesystem(struct archive *);
+__LA_DECL int	archive_read_disk_current_filesystem_is_synthetic(struct archive *);
+__LA_DECL int	archive_read_disk_current_filesystem_is_remote(struct archive *);
+/* Request that the access time of the entry visited by traversal be restored. */
+__LA_DECL int  archive_read_disk_set_atime_restored(struct archive *);
+/*
+ * Set behavior. The "flags" argument selects optional behavior.
+ */
+/* Request that the access time of the entry visited by traversal be restored.
+ * This is the same as archive_read_disk_set_atime_restored. */
+#define	ARCHIVE_READDISK_RESTORE_ATIME		(0x0001)
+/* Default: Do not skip an entry which has nodump flags. */
+#define	ARCHIVE_READDISK_HONOR_NODUMP		(0x0002)
+/* Default: Skip a mac resource fork file whose prefix is "._" because of
+ * using copyfile. */
+#define	ARCHIVE_READDISK_MAC_COPYFILE		(0x0004)
+/* Default: Traverse mount points. */
+#define	ARCHIVE_READDISK_NO_TRAVERSE_MOUNTS	(0x0008)
+/* Default: Xattrs are read from disk. */
+#define	ARCHIVE_READDISK_NO_XATTR		(0x0010)
+/* Default: ACLs are read from disk. */
+#define	ARCHIVE_READDISK_NO_ACL			(0x0020)
+/* Default: File flags are read from disk. */
+#define	ARCHIVE_READDISK_NO_FFLAGS		(0x0040)
+
+__LA_DECL int  archive_read_disk_set_behavior(struct archive *,
+		    int flags);
+
+/*
+ * Set archive_match object that will be used in archive_read_disk to
+ * know whether an entry should be skipped. The callback function
+ * _excluded_func will be invoked when an entry is skipped by the result
+ * of archive_match.
+ */
+__LA_DECL int	archive_read_disk_set_matching(struct archive *,
+		    struct archive *_matching, void (*_excluded_func)
+		    (struct archive *, void *, struct archive_entry *),
+		    void *_client_data);
+__LA_DECL int	archive_read_disk_set_metadata_filter_callback(struct archive *,
+		    int (*_metadata_filter_func)(struct archive *, void *,
+		    	struct archive_entry *), void *_client_data);
+
+/* Simplified cleanup interface;
+ * This calls archive_read_free() or archive_write_free() as needed. */
+__LA_DECL int	archive_free(struct archive *);
+
+/*
+ * Accessor functions to read/set various information in
+ * the struct archive object:
+ */
+
+/* Number of filters in the current filter pipeline. */
+/* Filter #0 is the one closest to the format, -1 is a synonym for the
+ * last filter, which is always the pseudo-filter that wraps the
+ * client callbacks. */
+__LA_DECL int		 archive_filter_count(struct archive *);
+__LA_DECL la_int64_t	 archive_filter_bytes(struct archive *, int);
+__LA_DECL int		 archive_filter_code(struct archive *, int);
+__LA_DECL const char *	 archive_filter_name(struct archive *, int);
+
+#if ARCHIVE_VERSION_NUMBER < 4000000
+/* These don't properly handle multiple filters, so are deprecated and
+ * will eventually be removed. */
+/* As of libarchive 3.0, this is an alias for archive_filter_bytes(a, -1); */
+__LA_DECL la_int64_t	 archive_position_compressed(struct archive *)
+				__LA_DEPRECATED;
+/* As of libarchive 3.0, this is an alias for archive_filter_bytes(a, 0); */
+__LA_DECL la_int64_t	 archive_position_uncompressed(struct archive *)
+				__LA_DEPRECATED;
+/* As of libarchive 3.0, this is an alias for archive_filter_name(a, 0); */
+__LA_DECL const char	*archive_compression_name(struct archive *)
+				__LA_DEPRECATED;
+/* As of libarchive 3.0, this is an alias for archive_filter_code(a, 0); */
+__LA_DECL int		 archive_compression(struct archive *)
+				__LA_DEPRECATED;
+#endif
+
+__LA_DECL int		 archive_errno(struct archive *);
+__LA_DECL const char	*archive_error_string(struct archive *);
+__LA_DECL const char	*archive_format_name(struct archive *);
+__LA_DECL int		 archive_format(struct archive *);
+__LA_DECL void		 archive_clear_error(struct archive *);
+__LA_DECL void		 archive_set_error(struct archive *, int _err,
+			    const char *fmt, ...) __LA_PRINTF(3, 4);
+__LA_DECL void		 archive_copy_error(struct archive *dest,
+			    struct archive *src);
+__LA_DECL int		 archive_file_count(struct archive *);
+
+/*
+ * ARCHIVE_MATCH API
+ */
+__LA_DECL struct archive *archive_match_new(void);
+__LA_DECL int	archive_match_free(struct archive *);
+
+/*
+ * Test if archive_entry is excluded.
+ * This is a convenience function. This is the same as calling all
+ * archive_match_path_excluded, archive_match_time_excluded
+ * and archive_match_owner_excluded.
+ */
+__LA_DECL int	archive_match_excluded(struct archive *,
+		    struct archive_entry *);
+
+/*
+ * Test if pathname is excluded. The conditions are set by following functions.
+ */
+__LA_DECL int	archive_match_path_excluded(struct archive *,
+		    struct archive_entry *);
+/* Control recursive inclusion of directory content when directory is included. Default on. */
+__LA_DECL int	archive_match_set_inclusion_recursion(struct archive *, int);
+/* Add exclusion pathname pattern. */
+__LA_DECL int	archive_match_exclude_pattern(struct archive *, const char *);
+__LA_DECL int	archive_match_exclude_pattern_w(struct archive *,
+		    const wchar_t *);
+/* Add exclusion pathname pattern from file. */
+__LA_DECL int	archive_match_exclude_pattern_from_file(struct archive *,
+		    const char *, int _nullSeparator);
+__LA_DECL int	archive_match_exclude_pattern_from_file_w(struct archive *,
+		    const wchar_t *, int _nullSeparator);
+/* Add inclusion pathname pattern. */
+__LA_DECL int	archive_match_include_pattern(struct archive *, const char *);
+__LA_DECL int	archive_match_include_pattern_w(struct archive *,
+		    const wchar_t *);
+/* Add inclusion pathname pattern from file. */
+__LA_DECL int	archive_match_include_pattern_from_file(struct archive *,
+		    const char *, int _nullSeparator);
+__LA_DECL int	archive_match_include_pattern_from_file_w(struct archive *,
+		    const wchar_t *, int _nullSeparator);
+/*
+ * How to get statistic information for inclusion patterns.
+ */
+/* Return the amount number of unmatched inclusion patterns. */
+__LA_DECL int	archive_match_path_unmatched_inclusions(struct archive *);
+/* Return the pattern of unmatched inclusion with ARCHIVE_OK.
+ * Return ARCHIVE_EOF if there is no inclusion pattern. */
+__LA_DECL int	archive_match_path_unmatched_inclusions_next(
+		    struct archive *, const char **);
+__LA_DECL int	archive_match_path_unmatched_inclusions_next_w(
+		    struct archive *, const wchar_t **);
+
+/*
+ * Test if a file is excluded by its time stamp.
+ * The conditions are set by following functions.
+ */
+__LA_DECL int	archive_match_time_excluded(struct archive *,
+		    struct archive_entry *);
+
+/*
+ * Flags to tell a matching type of time stamps. These are used for
+ * following functions.
+ */
+/* Time flag: mtime to be tested. */
+#define ARCHIVE_MATCH_MTIME	(0x0100)
+/* Time flag: ctime to be tested. */
+#define ARCHIVE_MATCH_CTIME	(0x0200)
+/* Comparison flag: Match the time if it is newer than. */
+#define ARCHIVE_MATCH_NEWER	(0x0001)
+/* Comparison flag: Match the time if it is older than. */
+#define ARCHIVE_MATCH_OLDER	(0x0002)
+/* Comparison flag: Match the time if it is equal to. */
+#define ARCHIVE_MATCH_EQUAL	(0x0010)
+/* Set inclusion time. */
+__LA_DECL int	archive_match_include_time(struct archive *, int _flag,
+		    time_t _sec, long _nsec);
+/* Set inclusion time by a date string. */
+__LA_DECL int	archive_match_include_date(struct archive *, int _flag,
+		    const char *_datestr);
+__LA_DECL int	archive_match_include_date_w(struct archive *, int _flag,
+		    const wchar_t *_datestr);
+/* Set inclusion time by a particular file. */
+__LA_DECL int	archive_match_include_file_time(struct archive *,
+		    int _flag, const char *_pathname);
+__LA_DECL int	archive_match_include_file_time_w(struct archive *,
+		    int _flag, const wchar_t *_pathname);
+/* Add exclusion entry. */
+__LA_DECL int	archive_match_exclude_entry(struct archive *,
+		    int _flag, struct archive_entry *);
+
+/*
+ * Test if a file is excluded by its uid ,gid, uname or gname.
+ * The conditions are set by following functions.
+ */
+__LA_DECL int	archive_match_owner_excluded(struct archive *,
+		    struct archive_entry *);
+/* Add inclusion uid, gid, uname and gname. */
+__LA_DECL int	archive_match_include_uid(struct archive *, la_int64_t);
+__LA_DECL int	archive_match_include_gid(struct archive *, la_int64_t);
+__LA_DECL int	archive_match_include_uname(struct archive *, const char *);
+__LA_DECL int	archive_match_include_uname_w(struct archive *,
+		    const wchar_t *);
+__LA_DECL int	archive_match_include_gname(struct archive *, const char *);
+__LA_DECL int	archive_match_include_gname_w(struct archive *,
+		    const wchar_t *);
+
+/* Utility functions */
+/* Convenience function to sort a NULL terminated list of strings */
+__LA_DECL int archive_utility_string_sort(char **);
+
+#ifdef __cplusplus
+}
+#endif
+
+/* These are meaningless outside of this header. */
+#undef __LA_DECL
+
+#endif /* !ARCHIVE_H_INCLUDED */
diff --git a/3rdp/win32.release/libarchive/include/archive_entry.h b/3rdp/win32.release/libarchive/include/archive_entry.h
new file mode 100644
index 0000000000000000000000000000000000000000..c0e75bf9f1029a81edd1f553608f861c26b40fa5
--- /dev/null
+++ b/3rdp/win32.release/libarchive/include/archive_entry.h
@@ -0,0 +1,721 @@
+/*-
+ * Copyright (c) 2003-2008 Tim Kientzle
+ * Copyright (c) 2016 Martin Matuska
+ * 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.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS 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 THE AUTHOR(S) 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.
+ *
+ * $FreeBSD: head/lib/libarchive/archive_entry.h 201096 2009-12-28 02:41:27Z kientzle $
+ */
+
+#ifndef ARCHIVE_ENTRY_H_INCLUDED
+#define	ARCHIVE_ENTRY_H_INCLUDED
+
+/* Note: Compiler will complain if this does not match archive.h! */
+#define	ARCHIVE_VERSION_NUMBER 3005001
+
+/*
+ * Note: archive_entry.h is for use outside of libarchive; the
+ * configuration headers (config.h, archive_platform.h, etc.) are
+ * purely internal.  Do NOT use HAVE_XXX configuration macros to
+ * control the behavior of this header!  If you must conditionalize,
+ * use predefined compiler and/or platform macros.
+ */
+
+#include <sys/types.h>
+#include <stddef.h>  /* for wchar_t */
+#include <stdint.h>
+#include <time.h>
+
+#if defined(_WIN32) && !defined(__CYGWIN__)
+#include <windows.h>
+#endif
+
+/* Get a suitable 64-bit integer type. */
+#if !defined(__LA_INT64_T_DEFINED)
+# if ARCHIVE_VERSION_NUMBER < 4000000
+#define __LA_INT64_T la_int64_t
+# endif
+#define __LA_INT64_T_DEFINED
+# if defined(_WIN32) && !defined(__CYGWIN__) && !defined(__WATCOMC__)
+typedef __int64 la_int64_t;
+# else
+#include <unistd.h>
+#  if defined(_SCO_DS) || defined(__osf__)
+typedef long long la_int64_t;
+#  else
+typedef int64_t la_int64_t;
+#  endif
+# endif
+#endif
+
+/* The la_ssize_t should match the type used in 'struct stat' */
+#if !defined(__LA_SSIZE_T_DEFINED)
+/* Older code relied on the __LA_SSIZE_T macro; after 4.0 we'll switch to the typedef exclusively. */
+# if ARCHIVE_VERSION_NUMBER < 4000000
+#define __LA_SSIZE_T la_ssize_t
+# endif
+#define __LA_SSIZE_T_DEFINED
+# if defined(_WIN32) && !defined(__CYGWIN__) && !defined(__WATCOMC__)
+#  if defined(_SSIZE_T_DEFINED) || defined(_SSIZE_T_)
+typedef ssize_t la_ssize_t;
+#  elif defined(_WIN64)
+typedef __int64 la_ssize_t;
+#  else
+typedef long la_ssize_t;
+#  endif
+# else
+# include <unistd.h>  /* ssize_t */
+typedef ssize_t la_ssize_t;
+# endif
+#endif
+
+/* Get a suitable definition for mode_t */
+#if ARCHIVE_VERSION_NUMBER >= 3999000
+/* Switch to plain 'int' for libarchive 4.0.  It's less broken than 'mode_t' */
+# define	__LA_MODE_T	int
+#elif defined(_WIN32) && !defined(__CYGWIN__) && !defined(__BORLANDC__) && !defined(__WATCOMC__)
+# define	__LA_MODE_T	unsigned short
+#else
+# define	__LA_MODE_T	mode_t
+#endif
+
+/* Large file support for Android */
+#ifdef __ANDROID__
+#include "android_lf.h"
+#endif
+
+/*
+ * On Windows, define LIBARCHIVE_STATIC if you're building or using a
+ * .lib.  The default here assumes you're building a DLL.  Only
+ * libarchive source should ever define __LIBARCHIVE_BUILD.
+ */
+#if ((defined __WIN32__) || (defined _WIN32) || defined(__CYGWIN__)) && (!defined LIBARCHIVE_STATIC)
+# ifdef __LIBARCHIVE_BUILD
+#  ifdef __GNUC__
+#   define __LA_DECL	__attribute__((dllexport)) extern
+#  else
+#   define __LA_DECL	__declspec(dllexport)
+#  endif
+# else
+#  ifdef __GNUC__
+#   define __LA_DECL
+#  else
+#   define __LA_DECL	__declspec(dllimport)
+#  endif
+# endif
+#else
+/* Static libraries on all platforms and shared libraries on non-Windows. */
+# define __LA_DECL
+#endif
+
+#if defined(__GNUC__) && __GNUC__ >= 3 && __GNUC_MINOR__ >= 1
+# define __LA_DEPRECATED __attribute__((deprecated))
+#else
+# define __LA_DEPRECATED
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*
+ * Description of an archive entry.
+ *
+ * You can think of this as "struct stat" with some text fields added in.
+ *
+ * TODO: Add "comment", "charset", and possibly other entries that are
+ * supported by "pax interchange" format.  However, GNU, ustar, cpio,
+ * and other variants don't support these features, so they're not an
+ * excruciatingly high priority right now.
+ *
+ * TODO: "pax interchange" format allows essentially arbitrary
+ * key/value attributes to be attached to any entry.  Supporting
+ * such extensions may make this library useful for special
+ * applications (e.g., a package manager could attach special
+ * package-management attributes to each entry).
+ */
+struct archive;
+struct archive_entry;
+
+/*
+ * File-type constants.  These are returned from archive_entry_filetype()
+ * and passed to archive_entry_set_filetype().
+ *
+ * These values match S_XXX defines on every platform I've checked,
+ * including Windows, AIX, Linux, Solaris, and BSD.  They're
+ * (re)defined here because platforms generally don't define the ones
+ * they don't support.  For example, Windows doesn't define S_IFLNK or
+ * S_IFBLK.  Instead of having a mass of conditional logic and system
+ * checks to define any S_XXX values that aren't supported locally,
+ * I've just defined a new set of such constants so that
+ * libarchive-based applications can manipulate and identify archive
+ * entries properly even if the hosting platform can't store them on
+ * disk.
+ *
+ * These values are also used directly within some portable formats,
+ * such as cpio.  If you find a platform that varies from these, the
+ * correct solution is to leave these alone and translate from these
+ * portable values to platform-native values when entries are read from
+ * or written to disk.
+ */
+/*
+ * In libarchive 4.0, we can drop the casts here.
+ * They're needed to work around Borland C's broken mode_t.
+ */
+#define AE_IFMT		((__LA_MODE_T)0170000)
+#define AE_IFREG	((__LA_MODE_T)0100000)
+#define AE_IFLNK	((__LA_MODE_T)0120000)
+#define AE_IFSOCK	((__LA_MODE_T)0140000)
+#define AE_IFCHR	((__LA_MODE_T)0020000)
+#define AE_IFBLK	((__LA_MODE_T)0060000)
+#define AE_IFDIR	((__LA_MODE_T)0040000)
+#define AE_IFIFO	((__LA_MODE_T)0010000)
+
+/*
+ * Symlink types
+ */
+#define AE_SYMLINK_TYPE_UNDEFINED	0
+#define AE_SYMLINK_TYPE_FILE		1
+#define AE_SYMLINK_TYPE_DIRECTORY	2
+
+/*
+ * Basic object manipulation
+ */
+
+__LA_DECL struct archive_entry	*archive_entry_clear(struct archive_entry *);
+/* The 'clone' function does a deep copy; all of the strings are copied too. */
+__LA_DECL struct archive_entry	*archive_entry_clone(struct archive_entry *);
+__LA_DECL void			 archive_entry_free(struct archive_entry *);
+__LA_DECL struct archive_entry	*archive_entry_new(void);
+
+/*
+ * This form of archive_entry_new2() will pull character-set
+ * conversion information from the specified archive handle.  The
+ * older archive_entry_new(void) form is equivalent to calling
+ * archive_entry_new2(NULL) and will result in the use of an internal
+ * default character-set conversion.
+ */
+__LA_DECL struct archive_entry	*archive_entry_new2(struct archive *);
+
+/*
+ * Retrieve fields from an archive_entry.
+ *
+ * There are a number of implicit conversions among these fields.  For
+ * example, if a regular string field is set and you read the _w wide
+ * character field, the entry will implicitly convert narrow-to-wide
+ * using the current locale.  Similarly, dev values are automatically
+ * updated when you write devmajor or devminor and vice versa.
+ *
+ * In addition, fields can be "set" or "unset."  Unset string fields
+ * return NULL, non-string fields have _is_set() functions to test
+ * whether they've been set.  You can "unset" a string field by
+ * assigning NULL; non-string fields have _unset() functions to
+ * unset them.
+ *
+ * Note: There is one ambiguity in the above; string fields will
+ * also return NULL when implicit character set conversions fail.
+ * This is usually what you want.
+ */
+__LA_DECL time_t	 archive_entry_atime(struct archive_entry *);
+__LA_DECL long		 archive_entry_atime_nsec(struct archive_entry *);
+__LA_DECL int		 archive_entry_atime_is_set(struct archive_entry *);
+__LA_DECL time_t	 archive_entry_birthtime(struct archive_entry *);
+__LA_DECL long		 archive_entry_birthtime_nsec(struct archive_entry *);
+__LA_DECL int		 archive_entry_birthtime_is_set(struct archive_entry *);
+__LA_DECL time_t	 archive_entry_ctime(struct archive_entry *);
+__LA_DECL long		 archive_entry_ctime_nsec(struct archive_entry *);
+__LA_DECL int		 archive_entry_ctime_is_set(struct archive_entry *);
+__LA_DECL dev_t		 archive_entry_dev(struct archive_entry *);
+__LA_DECL int		 archive_entry_dev_is_set(struct archive_entry *);
+__LA_DECL dev_t		 archive_entry_devmajor(struct archive_entry *);
+__LA_DECL dev_t		 archive_entry_devminor(struct archive_entry *);
+__LA_DECL __LA_MODE_T	 archive_entry_filetype(struct archive_entry *);
+__LA_DECL void		 archive_entry_fflags(struct archive_entry *,
+			    unsigned long * /* set */,
+			    unsigned long * /* clear */);
+__LA_DECL const char	*archive_entry_fflags_text(struct archive_entry *);
+__LA_DECL la_int64_t	 archive_entry_gid(struct archive_entry *);
+__LA_DECL const char	*archive_entry_gname(struct archive_entry *);
+__LA_DECL const char	*archive_entry_gname_utf8(struct archive_entry *);
+__LA_DECL const wchar_t	*archive_entry_gname_w(struct archive_entry *);
+__LA_DECL const char	*archive_entry_hardlink(struct archive_entry *);
+__LA_DECL const char	*archive_entry_hardlink_utf8(struct archive_entry *);
+__LA_DECL const wchar_t	*archive_entry_hardlink_w(struct archive_entry *);
+__LA_DECL la_int64_t	 archive_entry_ino(struct archive_entry *);
+__LA_DECL la_int64_t	 archive_entry_ino64(struct archive_entry *);
+__LA_DECL int		 archive_entry_ino_is_set(struct archive_entry *);
+__LA_DECL __LA_MODE_T	 archive_entry_mode(struct archive_entry *);
+__LA_DECL time_t	 archive_entry_mtime(struct archive_entry *);
+__LA_DECL long		 archive_entry_mtime_nsec(struct archive_entry *);
+__LA_DECL int		 archive_entry_mtime_is_set(struct archive_entry *);
+__LA_DECL unsigned int	 archive_entry_nlink(struct archive_entry *);
+__LA_DECL const char	*archive_entry_pathname(struct archive_entry *);
+__LA_DECL const char	*archive_entry_pathname_utf8(struct archive_entry *);
+__LA_DECL const wchar_t	*archive_entry_pathname_w(struct archive_entry *);
+__LA_DECL __LA_MODE_T	 archive_entry_perm(struct archive_entry *);
+__LA_DECL dev_t		 archive_entry_rdev(struct archive_entry *);
+__LA_DECL dev_t		 archive_entry_rdevmajor(struct archive_entry *);
+__LA_DECL dev_t		 archive_entry_rdevminor(struct archive_entry *);
+__LA_DECL const char	*archive_entry_sourcepath(struct archive_entry *);
+__LA_DECL const wchar_t	*archive_entry_sourcepath_w(struct archive_entry *);
+__LA_DECL la_int64_t	 archive_entry_size(struct archive_entry *);
+__LA_DECL int		 archive_entry_size_is_set(struct archive_entry *);
+__LA_DECL const char	*archive_entry_strmode(struct archive_entry *);
+__LA_DECL const char	*archive_entry_symlink(struct archive_entry *);
+__LA_DECL const char	*archive_entry_symlink_utf8(struct archive_entry *);
+__LA_DECL int		 archive_entry_symlink_type(struct archive_entry *);
+__LA_DECL const wchar_t	*archive_entry_symlink_w(struct archive_entry *);
+__LA_DECL la_int64_t	 archive_entry_uid(struct archive_entry *);
+__LA_DECL const char	*archive_entry_uname(struct archive_entry *);
+__LA_DECL const char	*archive_entry_uname_utf8(struct archive_entry *);
+__LA_DECL const wchar_t	*archive_entry_uname_w(struct archive_entry *);
+__LA_DECL int archive_entry_is_data_encrypted(struct archive_entry *);
+__LA_DECL int archive_entry_is_metadata_encrypted(struct archive_entry *);
+__LA_DECL int archive_entry_is_encrypted(struct archive_entry *);
+
+/*
+ * Set fields in an archive_entry.
+ *
+ * Note: Before libarchive 2.4, there were 'set' and 'copy' versions
+ * of the string setters.  'copy' copied the actual string, 'set' just
+ * stored the pointer.  In libarchive 2.4 and later, strings are
+ * always copied.
+ */
+
+__LA_DECL void	archive_entry_set_atime(struct archive_entry *, time_t, long);
+__LA_DECL void  archive_entry_unset_atime(struct archive_entry *);
+#if defined(_WIN32) && !defined(__CYGWIN__)
+__LA_DECL void archive_entry_copy_bhfi(struct archive_entry *, BY_HANDLE_FILE_INFORMATION *);
+#endif
+__LA_DECL void	archive_entry_set_birthtime(struct archive_entry *, time_t, long);
+__LA_DECL void  archive_entry_unset_birthtime(struct archive_entry *);
+__LA_DECL void	archive_entry_set_ctime(struct archive_entry *, time_t, long);
+__LA_DECL void  archive_entry_unset_ctime(struct archive_entry *);
+__LA_DECL void	archive_entry_set_dev(struct archive_entry *, dev_t);
+__LA_DECL void	archive_entry_set_devmajor(struct archive_entry *, dev_t);
+__LA_DECL void	archive_entry_set_devminor(struct archive_entry *, dev_t);
+__LA_DECL void	archive_entry_set_filetype(struct archive_entry *, unsigned int);
+__LA_DECL void	archive_entry_set_fflags(struct archive_entry *,
+	    unsigned long /* set */, unsigned long /* clear */);
+/* Returns pointer to start of first invalid token, or NULL if none. */
+/* Note that all recognized tokens are processed, regardless. */
+__LA_DECL const char *archive_entry_copy_fflags_text(struct archive_entry *,
+	    const char *);
+__LA_DECL const wchar_t *archive_entry_copy_fflags_text_w(struct archive_entry *,
+	    const wchar_t *);
+__LA_DECL void	archive_entry_set_gid(struct archive_entry *, la_int64_t);
+__LA_DECL void	archive_entry_set_gname(struct archive_entry *, const char *);
+__LA_DECL void	archive_entry_set_gname_utf8(struct archive_entry *, const char *);
+__LA_DECL void	archive_entry_copy_gname(struct archive_entry *, const char *);
+__LA_DECL void	archive_entry_copy_gname_w(struct archive_entry *, const wchar_t *);
+__LA_DECL int	archive_entry_update_gname_utf8(struct archive_entry *, const char *);
+__LA_DECL void	archive_entry_set_hardlink(struct archive_entry *, const char *);
+__LA_DECL void	archive_entry_set_hardlink_utf8(struct archive_entry *, const char *);
+__LA_DECL void	archive_entry_copy_hardlink(struct archive_entry *, const char *);
+__LA_DECL void	archive_entry_copy_hardlink_w(struct archive_entry *, const wchar_t *);
+__LA_DECL int	archive_entry_update_hardlink_utf8(struct archive_entry *, const char *);
+__LA_DECL void	archive_entry_set_ino(struct archive_entry *, la_int64_t);
+__LA_DECL void	archive_entry_set_ino64(struct archive_entry *, la_int64_t);
+__LA_DECL void	archive_entry_set_link(struct archive_entry *, const char *);
+__LA_DECL void	archive_entry_set_link_utf8(struct archive_entry *, const char *);
+__LA_DECL void	archive_entry_copy_link(struct archive_entry *, const char *);
+__LA_DECL void	archive_entry_copy_link_w(struct archive_entry *, const wchar_t *);
+__LA_DECL int	archive_entry_update_link_utf8(struct archive_entry *, const char *);
+__LA_DECL void	archive_entry_set_mode(struct archive_entry *, __LA_MODE_T);
+__LA_DECL void	archive_entry_set_mtime(struct archive_entry *, time_t, long);
+__LA_DECL void  archive_entry_unset_mtime(struct archive_entry *);
+__LA_DECL void	archive_entry_set_nlink(struct archive_entry *, unsigned int);
+__LA_DECL void	archive_entry_set_pathname(struct archive_entry *, const char *);
+__LA_DECL void	archive_entry_set_pathname_utf8(struct archive_entry *, const char *);
+__LA_DECL void	archive_entry_copy_pathname(struct archive_entry *, const char *);
+__LA_DECL void	archive_entry_copy_pathname_w(struct archive_entry *, const wchar_t *);
+__LA_DECL int	archive_entry_update_pathname_utf8(struct archive_entry *, const char *);
+__LA_DECL void	archive_entry_set_perm(struct archive_entry *, __LA_MODE_T);
+__LA_DECL void	archive_entry_set_rdev(struct archive_entry *, dev_t);
+__LA_DECL void	archive_entry_set_rdevmajor(struct archive_entry *, dev_t);
+__LA_DECL void	archive_entry_set_rdevminor(struct archive_entry *, dev_t);
+__LA_DECL void	archive_entry_set_size(struct archive_entry *, la_int64_t);
+__LA_DECL void	archive_entry_unset_size(struct archive_entry *);
+__LA_DECL void	archive_entry_copy_sourcepath(struct archive_entry *, const char *);
+__LA_DECL void	archive_entry_copy_sourcepath_w(struct archive_entry *, const wchar_t *);
+__LA_DECL void	archive_entry_set_symlink(struct archive_entry *, const char *);
+__LA_DECL void	archive_entry_set_symlink_type(struct archive_entry *, int);
+__LA_DECL void	archive_entry_set_symlink_utf8(struct archive_entry *, const char *);
+__LA_DECL void	archive_entry_copy_symlink(struct archive_entry *, const char *);
+__LA_DECL void	archive_entry_copy_symlink_w(struct archive_entry *, const wchar_t *);
+__LA_DECL int	archive_entry_update_symlink_utf8(struct archive_entry *, const char *);
+__LA_DECL void	archive_entry_set_uid(struct archive_entry *, la_int64_t);
+__LA_DECL void	archive_entry_set_uname(struct archive_entry *, const char *);
+__LA_DECL void	archive_entry_set_uname_utf8(struct archive_entry *, const char *);
+__LA_DECL void	archive_entry_copy_uname(struct archive_entry *, const char *);
+__LA_DECL void	archive_entry_copy_uname_w(struct archive_entry *, const wchar_t *);
+__LA_DECL int	archive_entry_update_uname_utf8(struct archive_entry *, const char *);
+__LA_DECL void	archive_entry_set_is_data_encrypted(struct archive_entry *, char is_encrypted);
+__LA_DECL void	archive_entry_set_is_metadata_encrypted(struct archive_entry *, char is_encrypted);
+/*
+ * Routines to bulk copy fields to/from a platform-native "struct
+ * stat."  Libarchive used to just store a struct stat inside of each
+ * archive_entry object, but this created issues when trying to
+ * manipulate archives on systems different than the ones they were
+ * created on.
+ *
+ * TODO: On Linux and other LFS systems, provide both stat32 and
+ * stat64 versions of these functions and all of the macro glue so
+ * that archive_entry_stat is magically defined to
+ * archive_entry_stat32 or archive_entry_stat64 as appropriate.
+ */
+__LA_DECL const struct stat	*archive_entry_stat(struct archive_entry *);
+__LA_DECL void	archive_entry_copy_stat(struct archive_entry *, const struct stat *);
+
+/*
+ * Storage for Mac OS-specific AppleDouble metadata information.
+ * Apple-format tar files store a separate binary blob containing
+ * encoded metadata with ACL, extended attributes, etc.
+ * This provides a place to store that blob.
+ */
+
+__LA_DECL const void * archive_entry_mac_metadata(struct archive_entry *, size_t *);
+__LA_DECL void archive_entry_copy_mac_metadata(struct archive_entry *, const void *, size_t);
+
+/*
+ * Digest routine. This is used to query the raw hex digest for the
+ * given entry. The type of digest is provided as an argument.
+ */
+#define ARCHIVE_ENTRY_DIGEST_MD5              0x00000001
+#define ARCHIVE_ENTRY_DIGEST_RMD160           0x00000002
+#define ARCHIVE_ENTRY_DIGEST_SHA1             0x00000003
+#define ARCHIVE_ENTRY_DIGEST_SHA256           0x00000004
+#define ARCHIVE_ENTRY_DIGEST_SHA384           0x00000005
+#define ARCHIVE_ENTRY_DIGEST_SHA512           0x00000006
+
+__LA_DECL const unsigned char * archive_entry_digest(struct archive_entry *, int /* type */);
+
+/*
+ * ACL routines.  This used to simply store and return text-format ACL
+ * strings, but that proved insufficient for a number of reasons:
+ *   = clients need control over uname/uid and gname/gid mappings
+ *   = there are many different ACL text formats
+ *   = would like to be able to read/convert archives containing ACLs
+ *     on platforms that lack ACL libraries
+ *
+ *  This last point, in particular, forces me to implement a reasonably
+ *  complete set of ACL support routines.
+ */
+
+/*
+ * Permission bits.
+ */
+#define	ARCHIVE_ENTRY_ACL_EXECUTE             0x00000001
+#define	ARCHIVE_ENTRY_ACL_WRITE               0x00000002
+#define	ARCHIVE_ENTRY_ACL_READ                0x00000004
+#define	ARCHIVE_ENTRY_ACL_READ_DATA           0x00000008
+#define	ARCHIVE_ENTRY_ACL_LIST_DIRECTORY      0x00000008
+#define	ARCHIVE_ENTRY_ACL_WRITE_DATA          0x00000010
+#define	ARCHIVE_ENTRY_ACL_ADD_FILE            0x00000010
+#define	ARCHIVE_ENTRY_ACL_APPEND_DATA         0x00000020
+#define	ARCHIVE_ENTRY_ACL_ADD_SUBDIRECTORY    0x00000020
+#define	ARCHIVE_ENTRY_ACL_READ_NAMED_ATTRS    0x00000040
+#define	ARCHIVE_ENTRY_ACL_WRITE_NAMED_ATTRS   0x00000080
+#define	ARCHIVE_ENTRY_ACL_DELETE_CHILD        0x00000100
+#define	ARCHIVE_ENTRY_ACL_READ_ATTRIBUTES     0x00000200
+#define	ARCHIVE_ENTRY_ACL_WRITE_ATTRIBUTES    0x00000400
+#define	ARCHIVE_ENTRY_ACL_DELETE              0x00000800
+#define	ARCHIVE_ENTRY_ACL_READ_ACL            0x00001000
+#define	ARCHIVE_ENTRY_ACL_WRITE_ACL           0x00002000
+#define	ARCHIVE_ENTRY_ACL_WRITE_OWNER         0x00004000
+#define	ARCHIVE_ENTRY_ACL_SYNCHRONIZE         0x00008000
+
+#define	ARCHIVE_ENTRY_ACL_PERMS_POSIX1E			\
+	(ARCHIVE_ENTRY_ACL_EXECUTE			\
+	    | ARCHIVE_ENTRY_ACL_WRITE			\
+	    | ARCHIVE_ENTRY_ACL_READ)
+
+#define ARCHIVE_ENTRY_ACL_PERMS_NFS4			\
+	(ARCHIVE_ENTRY_ACL_EXECUTE			\
+	    | ARCHIVE_ENTRY_ACL_READ_DATA		\
+	    | ARCHIVE_ENTRY_ACL_LIST_DIRECTORY 		\
+	    | ARCHIVE_ENTRY_ACL_WRITE_DATA		\
+	    | ARCHIVE_ENTRY_ACL_ADD_FILE		\
+	    | ARCHIVE_ENTRY_ACL_APPEND_DATA		\
+	    | ARCHIVE_ENTRY_ACL_ADD_SUBDIRECTORY	\
+	    | ARCHIVE_ENTRY_ACL_READ_NAMED_ATTRS	\
+	    | ARCHIVE_ENTRY_ACL_WRITE_NAMED_ATTRS	\
+	    | ARCHIVE_ENTRY_ACL_DELETE_CHILD		\
+	    | ARCHIVE_ENTRY_ACL_READ_ATTRIBUTES		\
+	    | ARCHIVE_ENTRY_ACL_WRITE_ATTRIBUTES	\
+	    | ARCHIVE_ENTRY_ACL_DELETE			\
+	    | ARCHIVE_ENTRY_ACL_READ_ACL		\
+	    | ARCHIVE_ENTRY_ACL_WRITE_ACL		\
+	    | ARCHIVE_ENTRY_ACL_WRITE_OWNER		\
+	    | ARCHIVE_ENTRY_ACL_SYNCHRONIZE)
+
+/*
+ * Inheritance values (NFS4 ACLs only); included in permset.
+ */
+#define	ARCHIVE_ENTRY_ACL_ENTRY_INHERITED                   0x01000000
+#define	ARCHIVE_ENTRY_ACL_ENTRY_FILE_INHERIT                0x02000000
+#define	ARCHIVE_ENTRY_ACL_ENTRY_DIRECTORY_INHERIT           0x04000000
+#define	ARCHIVE_ENTRY_ACL_ENTRY_NO_PROPAGATE_INHERIT        0x08000000
+#define	ARCHIVE_ENTRY_ACL_ENTRY_INHERIT_ONLY                0x10000000
+#define	ARCHIVE_ENTRY_ACL_ENTRY_SUCCESSFUL_ACCESS           0x20000000
+#define	ARCHIVE_ENTRY_ACL_ENTRY_FAILED_ACCESS               0x40000000
+
+#define	ARCHIVE_ENTRY_ACL_INHERITANCE_NFS4			\
+	(ARCHIVE_ENTRY_ACL_ENTRY_FILE_INHERIT			\
+	    | ARCHIVE_ENTRY_ACL_ENTRY_DIRECTORY_INHERIT		\
+	    | ARCHIVE_ENTRY_ACL_ENTRY_NO_PROPAGATE_INHERIT	\
+	    | ARCHIVE_ENTRY_ACL_ENTRY_INHERIT_ONLY		\
+	    | ARCHIVE_ENTRY_ACL_ENTRY_SUCCESSFUL_ACCESS		\
+	    | ARCHIVE_ENTRY_ACL_ENTRY_FAILED_ACCESS		\
+	    | ARCHIVE_ENTRY_ACL_ENTRY_INHERITED)
+
+/* We need to be able to specify combinations of these. */
+#define	ARCHIVE_ENTRY_ACL_TYPE_ACCESS	0x00000100  /* POSIX.1e only */
+#define	ARCHIVE_ENTRY_ACL_TYPE_DEFAULT	0x00000200  /* POSIX.1e only */
+#define	ARCHIVE_ENTRY_ACL_TYPE_ALLOW	0x00000400 /* NFS4 only */
+#define	ARCHIVE_ENTRY_ACL_TYPE_DENY	0x00000800 /* NFS4 only */
+#define	ARCHIVE_ENTRY_ACL_TYPE_AUDIT	0x00001000 /* NFS4 only */
+#define	ARCHIVE_ENTRY_ACL_TYPE_ALARM	0x00002000 /* NFS4 only */
+#define	ARCHIVE_ENTRY_ACL_TYPE_POSIX1E	(ARCHIVE_ENTRY_ACL_TYPE_ACCESS \
+	    | ARCHIVE_ENTRY_ACL_TYPE_DEFAULT)
+#define	ARCHIVE_ENTRY_ACL_TYPE_NFS4	(ARCHIVE_ENTRY_ACL_TYPE_ALLOW \
+	    | ARCHIVE_ENTRY_ACL_TYPE_DENY \
+	    | ARCHIVE_ENTRY_ACL_TYPE_AUDIT \
+	    | ARCHIVE_ENTRY_ACL_TYPE_ALARM)
+
+/* Tag values mimic POSIX.1e */
+#define	ARCHIVE_ENTRY_ACL_USER		10001	/* Specified user. */
+#define	ARCHIVE_ENTRY_ACL_USER_OBJ 	10002	/* User who owns the file. */
+#define	ARCHIVE_ENTRY_ACL_GROUP		10003	/* Specified group. */
+#define	ARCHIVE_ENTRY_ACL_GROUP_OBJ	10004	/* Group who owns the file. */
+#define	ARCHIVE_ENTRY_ACL_MASK		10005	/* Modify group access (POSIX.1e only) */
+#define	ARCHIVE_ENTRY_ACL_OTHER		10006	/* Public (POSIX.1e only) */
+#define	ARCHIVE_ENTRY_ACL_EVERYONE	10107   /* Everyone (NFS4 only) */
+
+/*
+ * Set the ACL by clearing it and adding entries one at a time.
+ * Unlike the POSIX.1e ACL routines, you must specify the type
+ * (access/default) for each entry.  Internally, the ACL data is just
+ * a soup of entries.  API calls here allow you to retrieve just the
+ * entries of interest.  This design (which goes against the spirit of
+ * POSIX.1e) is useful for handling archive formats that combine
+ * default and access information in a single ACL list.
+ */
+__LA_DECL void	 archive_entry_acl_clear(struct archive_entry *);
+__LA_DECL int	 archive_entry_acl_add_entry(struct archive_entry *,
+	    int /* type */, int /* permset */, int /* tag */,
+	    int /* qual */, const char * /* name */);
+__LA_DECL int	 archive_entry_acl_add_entry_w(struct archive_entry *,
+	    int /* type */, int /* permset */, int /* tag */,
+	    int /* qual */, const wchar_t * /* name */);
+
+/*
+ * To retrieve the ACL, first "reset", then repeatedly ask for the
+ * "next" entry.  The want_type parameter allows you to request only
+ * certain types of entries.
+ */
+__LA_DECL int	 archive_entry_acl_reset(struct archive_entry *, int /* want_type */);
+__LA_DECL int	 archive_entry_acl_next(struct archive_entry *, int /* want_type */,
+	    int * /* type */, int * /* permset */, int * /* tag */,
+	    int * /* qual */, const char ** /* name */);
+
+/*
+ * Construct a text-format ACL.  The flags argument is a bitmask that
+ * can include any of the following:
+ *
+ * Flags only for archive entries with POSIX.1e ACL:
+ * ARCHIVE_ENTRY_ACL_TYPE_ACCESS - Include POSIX.1e "access" entries.
+ * ARCHIVE_ENTRY_ACL_TYPE_DEFAULT - Include POSIX.1e "default" entries.
+ * ARCHIVE_ENTRY_ACL_STYLE_MARK_DEFAULT - Include "default:" before each
+ *    default ACL entry.
+ * ARCHIVE_ENTRY_ACL_STYLE_SOLARIS - Output only one colon after "other" and
+ *    "mask" entries.
+ *
+ * Flags only for archive entries with NFSv4 ACL:
+ * ARCHIVE_ENTRY_ACL_STYLE_COMPACT - Do not output the minus character for
+ *    unset permissions and flags in NFSv4 ACL permission and flag fields
+ *
+ * Flags for for archive entries with POSIX.1e ACL or NFSv4 ACL:
+ * ARCHIVE_ENTRY_ACL_STYLE_EXTRA_ID - Include extra numeric ID field in
+ *    each ACL entry.
+ * ARCHIVE_ENTRY_ACL_STYLE_SEPARATOR_COMMA - Separate entries with comma
+ *    instead of newline.
+ */
+#define	ARCHIVE_ENTRY_ACL_STYLE_EXTRA_ID	0x00000001
+#define	ARCHIVE_ENTRY_ACL_STYLE_MARK_DEFAULT	0x00000002
+#define	ARCHIVE_ENTRY_ACL_STYLE_SOLARIS		0x00000004
+#define	ARCHIVE_ENTRY_ACL_STYLE_SEPARATOR_COMMA	0x00000008
+#define	ARCHIVE_ENTRY_ACL_STYLE_COMPACT		0x00000010
+
+__LA_DECL wchar_t *archive_entry_acl_to_text_w(struct archive_entry *,
+	    la_ssize_t * /* len */, int /* flags */);
+__LA_DECL char *archive_entry_acl_to_text(struct archive_entry *,
+	    la_ssize_t * /* len */, int /* flags */);
+__LA_DECL int archive_entry_acl_from_text_w(struct archive_entry *,
+	    const wchar_t * /* wtext */, int /* type */);
+__LA_DECL int archive_entry_acl_from_text(struct archive_entry *,
+	    const char * /* text */, int /* type */);
+
+/* Deprecated constants */
+#define	OLD_ARCHIVE_ENTRY_ACL_STYLE_EXTRA_ID		1024
+#define	OLD_ARCHIVE_ENTRY_ACL_STYLE_MARK_DEFAULT	2048
+
+/* Deprecated functions */
+__LA_DECL const wchar_t	*archive_entry_acl_text_w(struct archive_entry *,
+		    int /* flags */) __LA_DEPRECATED;
+__LA_DECL const char *archive_entry_acl_text(struct archive_entry *,
+		    int /* flags */) __LA_DEPRECATED;
+
+/* Return bitmask of ACL types in an archive entry */
+__LA_DECL int	 archive_entry_acl_types(struct archive_entry *);
+
+/* Return a count of entries matching 'want_type' */
+__LA_DECL int	 archive_entry_acl_count(struct archive_entry *, int /* want_type */);
+
+/* Return an opaque ACL object. */
+/* There's not yet anything clients can actually do with this... */
+struct archive_acl;
+__LA_DECL struct archive_acl *archive_entry_acl(struct archive_entry *);
+
+/*
+ * extended attributes
+ */
+
+__LA_DECL void	 archive_entry_xattr_clear(struct archive_entry *);
+__LA_DECL void	 archive_entry_xattr_add_entry(struct archive_entry *,
+	    const char * /* name */, const void * /* value */,
+	    size_t /* size */);
+
+/*
+ * To retrieve the xattr list, first "reset", then repeatedly ask for the
+ * "next" entry.
+ */
+
+__LA_DECL int	archive_entry_xattr_count(struct archive_entry *);
+__LA_DECL int	archive_entry_xattr_reset(struct archive_entry *);
+__LA_DECL int	archive_entry_xattr_next(struct archive_entry *,
+	    const char ** /* name */, const void ** /* value */, size_t *);
+
+/*
+ * sparse
+ */
+
+__LA_DECL void	 archive_entry_sparse_clear(struct archive_entry *);
+__LA_DECL void	 archive_entry_sparse_add_entry(struct archive_entry *,
+	    la_int64_t /* offset */, la_int64_t /* length */);
+
+/*
+ * To retrieve the xattr list, first "reset", then repeatedly ask for the
+ * "next" entry.
+ */
+
+__LA_DECL int	archive_entry_sparse_count(struct archive_entry *);
+__LA_DECL int	archive_entry_sparse_reset(struct archive_entry *);
+__LA_DECL int	archive_entry_sparse_next(struct archive_entry *,
+	    la_int64_t * /* offset */, la_int64_t * /* length */);
+
+/*
+ * Utility to match up hardlinks.
+ *
+ * The 'struct archive_entry_linkresolver' is a cache of archive entries
+ * for files with multiple links.  Here's how to use it:
+ *   1. Create a lookup object with archive_entry_linkresolver_new()
+ *   2. Tell it the archive format you're using.
+ *   3. Hand each archive_entry to archive_entry_linkify().
+ *      That function will return 0, 1, or 2 entries that should
+ *      be written.
+ *   4. Call archive_entry_linkify(resolver, NULL) until
+ *      no more entries are returned.
+ *   5. Call archive_entry_linkresolver_free(resolver) to free resources.
+ *
+ * The entries returned have their hardlink and size fields updated
+ * appropriately.  If an entry is passed in that does not refer to
+ * a file with multiple links, it is returned unchanged.  The intention
+ * is that you should be able to simply filter all entries through
+ * this machine.
+ *
+ * To make things more efficient, be sure that each entry has a valid
+ * nlinks value.  The hardlink cache uses this to track when all links
+ * have been found.  If the nlinks value is zero, it will keep every
+ * name in the cache indefinitely, which can use a lot of memory.
+ *
+ * Note that archive_entry_size() is reset to zero if the file
+ * body should not be written to the archive.  Pay attention!
+ */
+struct archive_entry_linkresolver;
+
+/*
+ * There are three different strategies for marking hardlinks.
+ * The descriptions below name them after the best-known
+ * formats that rely on each strategy:
+ *
+ * "Old cpio" is the simplest, it always returns any entry unmodified.
+ *    As far as I know, only cpio formats use this.  Old cpio archives
+ *    store every link with the full body; the onus is on the dearchiver
+ *    to detect and properly link the files as they are restored.
+ * "tar" is also pretty simple; it caches a copy the first time it sees
+ *    any link.  Subsequent appearances are modified to be hardlink
+ *    references to the first one without any body.  Used by all tar
+ *    formats, although the newest tar formats permit the "old cpio" strategy
+ *    as well.  This strategy is very simple for the dearchiver,
+ *    and reasonably straightforward for the archiver.
+ * "new cpio" is trickier.  It stores the body only with the last
+ *    occurrence.  The complication is that we might not
+ *    see every link to a particular file in a single session, so
+ *    there's no easy way to know when we've seen the last occurrence.
+ *    The solution here is to queue one link until we see the next.
+ *    At the end of the session, you can enumerate any remaining
+ *    entries by calling archive_entry_linkify(NULL) and store those
+ *    bodies.  If you have a file with three links l1, l2, and l3,
+ *    you'll get the following behavior if you see all three links:
+ *           linkify(l1) => NULL   (the resolver stores l1 internally)
+ *           linkify(l2) => l1     (resolver stores l2, you write l1)
+ *           linkify(l3) => l2, l3 (all links seen, you can write both).
+ *    If you only see l1 and l2, you'll get this behavior:
+ *           linkify(l1) => NULL
+ *           linkify(l2) => l1
+ *           linkify(NULL) => l2   (at end, you retrieve remaining links)
+ *    As the name suggests, this strategy is used by newer cpio variants.
+ *    It's noticeably more complex for the archiver, slightly more complex
+ *    for the dearchiver than the tar strategy, but makes it straightforward
+ *    to restore a file using any link by simply continuing to scan until
+ *    you see a link that is stored with a body.  In contrast, the tar
+ *    strategy requires you to rescan the archive from the beginning to
+ *    correctly extract an arbitrary link.
+ */
+
+__LA_DECL struct archive_entry_linkresolver *archive_entry_linkresolver_new(void);
+__LA_DECL void archive_entry_linkresolver_set_strategy(
+	struct archive_entry_linkresolver *, int /* format_code */);
+__LA_DECL void archive_entry_linkresolver_free(struct archive_entry_linkresolver *);
+__LA_DECL void archive_entry_linkify(struct archive_entry_linkresolver *,
+    struct archive_entry **, struct archive_entry **);
+__LA_DECL struct archive_entry *archive_entry_partial_links(
+    struct archive_entry_linkresolver *res, unsigned int *links);
+#ifdef __cplusplus
+}
+#endif
+
+/* This is meaningless outside of this header. */
+#undef __LA_DECL
+
+#endif /* !ARCHIVE_ENTRY_H_INCLUDED */
diff --git a/3rdp/win32.release/libarchive/libarchive.props b/3rdp/win32.release/libarchive/libarchive.props
new file mode 100644
index 0000000000000000000000000000000000000000..7928e34137775fdf97c544a014caa5a17e146427
--- /dev/null
+++ b/3rdp/win32.release/libarchive/libarchive.props
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <ImportGroup Label="PropertySheets" />
+  <PropertyGroup Label="UserMacros" />
+  <PropertyGroup>
+    <_PropertySheetDisplayName>Archive Library</_PropertySheetDisplayName>
+  </PropertyGroup>
+  <ItemDefinitionGroup>
+    <ClCompile>
+      <AdditionalIncludeDirectories>$(MSBuildThisFileDirectory)/include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
+    </ClCompile>
+    <Link>
+      <AdditionalDependencies>$(MSBuildThisFileDirectory)/bin/archive.lib;%(AdditionalDependencies)</AdditionalDependencies>
+    </Link>
+  </ItemDefinitionGroup>
+  <ItemGroup />
+</Project>
\ No newline at end of file
diff --git a/3rdp/win32.release/zlib/bin/zlib1.dll b/3rdp/win32.release/zlib/bin/zlib1.dll
new file mode 100644
index 0000000000000000000000000000000000000000..31996cd3e2e9c86188c25882a2d5671e926c0740
Binary files /dev/null and b/3rdp/win32.release/zlib/bin/zlib1.dll differ
diff --git a/3rdp/win32.release/zlib/include/zconf.h b/3rdp/win32.release/zlib/include/zconf.h
new file mode 100644
index 0000000000000000000000000000000000000000..03a9431c8be2b44f2b1159308454a0f9c628597c
--- /dev/null
+++ b/3rdp/win32.release/zlib/include/zconf.h
@@ -0,0 +1,332 @@
+/* zconf.h -- configuration of the zlib compression library
+ * Copyright (C) 1995-2005 Jean-loup Gailly.
+ * For conditions of distribution and use, see copyright notice in zlib.h
+ */
+
+/* @(#) $Id$ */
+
+#ifndef ZCONF_H
+#define ZCONF_H
+
+/*
+ * If you *really* need a unique prefix for all types and library functions,
+ * compile with -DZ_PREFIX. The "standard" zlib should be compiled without it.
+ */
+#ifdef Z_PREFIX
+#  define deflateInit_          z_deflateInit_
+#  define deflate               z_deflate
+#  define deflateEnd            z_deflateEnd
+#  define inflateInit_          z_inflateInit_
+#  define inflate               z_inflate
+#  define inflateEnd            z_inflateEnd
+#  define deflateInit2_         z_deflateInit2_
+#  define deflateSetDictionary  z_deflateSetDictionary
+#  define deflateCopy           z_deflateCopy
+#  define deflateReset          z_deflateReset
+#  define deflateParams         z_deflateParams
+#  define deflateBound          z_deflateBound
+#  define deflatePrime          z_deflatePrime
+#  define inflateInit2_         z_inflateInit2_
+#  define inflateSetDictionary  z_inflateSetDictionary
+#  define inflateSync           z_inflateSync
+#  define inflateSyncPoint      z_inflateSyncPoint
+#  define inflateCopy           z_inflateCopy
+#  define inflateReset          z_inflateReset
+#  define inflateBack           z_inflateBack
+#  define inflateBackEnd        z_inflateBackEnd
+#  define compress              z_compress
+#  define compress2             z_compress2
+#  define compressBound         z_compressBound
+#  define uncompress            z_uncompress
+#  define adler32               z_adler32
+#  define crc32                 z_crc32
+#  define get_crc_table         z_get_crc_table
+#  define zError                z_zError
+
+#  define alloc_func            z_alloc_func
+#  define free_func             z_free_func
+#  define in_func               z_in_func
+#  define out_func              z_out_func
+#  define Byte                  z_Byte
+#  define uInt                  z_uInt
+#  define uLong                 z_uLong
+#  define Bytef                 z_Bytef
+#  define charf                 z_charf
+#  define intf                  z_intf
+#  define uIntf                 z_uIntf
+#  define uLongf                z_uLongf
+#  define voidpf                z_voidpf
+#  define voidp                 z_voidp
+#endif
+
+#if defined(__MSDOS__) && !defined(MSDOS)
+#  define MSDOS
+#endif
+#if (defined(OS_2) || defined(__OS2__)) && !defined(OS2)
+#  define OS2
+#endif
+#if defined(_WINDOWS) && !defined(WINDOWS)
+#  define WINDOWS
+#endif
+#if defined(_WIN32) || defined(_WIN32_WCE) || defined(__WIN32__)
+#  ifndef WIN32
+#    define WIN32
+#  endif
+#endif
+#if (defined(MSDOS) || defined(OS2) || defined(WINDOWS)) && !defined(WIN32)
+#  if !defined(__GNUC__) && !defined(__FLAT__) && !defined(__386__)
+#    ifndef SYS16BIT
+#      define SYS16BIT
+#    endif
+#  endif
+#endif
+
+/*
+ * Compile with -DMAXSEG_64K if the alloc function cannot allocate more
+ * than 64k bytes at a time (needed on systems with 16-bit int).
+ */
+#ifdef SYS16BIT
+#  define MAXSEG_64K
+#endif
+#ifdef MSDOS
+#  define UNALIGNED_OK
+#endif
+
+#ifdef __STDC_VERSION__
+#  ifndef STDC
+#    define STDC
+#  endif
+#  if __STDC_VERSION__ >= 199901L
+#    ifndef STDC99
+#      define STDC99
+#    endif
+#  endif
+#endif
+#if !defined(STDC) && (defined(__STDC__) || defined(__cplusplus))
+#  define STDC
+#endif
+#if !defined(STDC) && (defined(__GNUC__) || defined(__BORLANDC__))
+#  define STDC
+#endif
+#if !defined(STDC) && (defined(MSDOS) || defined(WINDOWS) || defined(WIN32))
+#  define STDC
+#endif
+#if !defined(STDC) && (defined(OS2) || defined(__HOS_AIX__))
+#  define STDC
+#endif
+
+#if defined(__OS400__) && !defined(STDC)    /* iSeries (formerly AS/400). */
+#  define STDC
+#endif
+
+#ifndef STDC
+#  ifndef const /* cannot use !defined(STDC) && !defined(const) on Mac */
+#    define const       /* note: need a more gentle solution here */
+#  endif
+#endif
+
+/* Some Mac compilers merge all .h files incorrectly: */
+#if defined(__MWERKS__)||defined(applec)||defined(THINK_C)||defined(__SC__)
+#  define NO_DUMMY_DECL
+#endif
+
+/* Maximum value for memLevel in deflateInit2 */
+#ifndef MAX_MEM_LEVEL
+#  ifdef MAXSEG_64K
+#    define MAX_MEM_LEVEL 8
+#  else
+#    define MAX_MEM_LEVEL 9
+#  endif
+#endif
+
+/* Maximum value for windowBits in deflateInit2 and inflateInit2.
+ * WARNING: reducing MAX_WBITS makes minigzip unable to extract .gz files
+ * created by gzip. (Files created by minigzip can still be extracted by
+ * gzip.)
+ */
+#ifndef MAX_WBITS
+#  define MAX_WBITS   15 /* 32K LZ77 window */
+#endif
+
+/* The memory requirements for deflate are (in bytes):
+            (1 << (windowBits+2)) +  (1 << (memLevel+9))
+ that is: 128K for windowBits=15  +  128K for memLevel = 8  (default values)
+ plus a few kilobytes for small objects. For example, if you want to reduce
+ the default memory requirements from 256K to 128K, compile with
+     make CFLAGS="-O -DMAX_WBITS=14 -DMAX_MEM_LEVEL=7"
+ Of course this will generally degrade compression (there's no free lunch).
+
+   The memory requirements for inflate are (in bytes) 1 << windowBits
+ that is, 32K for windowBits=15 (default value) plus a few kilobytes
+ for small objects.
+*/
+
+                        /* Type declarations */
+
+#ifndef OF /* function prototypes */
+#  ifdef STDC
+#    define OF(args)  args
+#  else
+#    define OF(args)  ()
+#  endif
+#endif
+
+/* The following definitions for FAR are needed only for MSDOS mixed
+ * model programming (small or medium model with some far allocations).
+ * This was tested only with MSC; for other MSDOS compilers you may have
+ * to define NO_MEMCPY in zutil.h.  If you don't need the mixed model,
+ * just define FAR to be empty.
+ */
+#ifdef SYS16BIT
+#  if defined(M_I86SM) || defined(M_I86MM)
+     /* MSC small or medium model */
+#    define SMALL_MEDIUM
+#    ifdef _MSC_VER
+#      define FAR _far
+#    else
+#      define FAR far
+#    endif
+#  endif
+#  if (defined(__SMALL__) || defined(__MEDIUM__))
+     /* Turbo C small or medium model */
+#    define SMALL_MEDIUM
+#    ifdef __BORLANDC__
+#      define FAR _far
+#    else
+#      define FAR far
+#    endif
+#  endif
+#endif
+
+#if defined(WINDOWS) || defined(WIN32)
+   /* If building or using zlib as a DLL, define ZLIB_DLL.
+    * This is not mandatory, but it offers a little performance increase.
+    */
+#  ifdef ZLIB_DLL
+#    if defined(WIN32) && (!defined(__BORLANDC__) || (__BORLANDC__ >= 0x500))
+#      ifdef ZLIB_INTERNAL
+#        define ZEXTERN extern __declspec(dllexport)
+#      else
+#        define ZEXTERN extern __declspec(dllimport)
+#      endif
+#    endif
+#  endif  /* ZLIB_DLL */
+   /* If building or using zlib with the WINAPI/WINAPIV calling convention,
+    * define ZLIB_WINAPI.
+    * Caution: the standard ZLIB1.DLL is NOT compiled using ZLIB_WINAPI.
+    */
+#  ifdef ZLIB_WINAPI
+#    ifdef FAR
+#      undef FAR
+#    endif
+#    include <windows.h>
+     /* No need for _export, use ZLIB.DEF instead. */
+     /* For complete Windows compatibility, use WINAPI, not __stdcall. */
+#    define ZEXPORT WINAPI
+#    ifdef WIN32
+#      define ZEXPORTVA WINAPIV
+#    else
+#      define ZEXPORTVA FAR CDECL
+#    endif
+#  endif
+#endif
+
+#if defined (__BEOS__)
+#  ifdef ZLIB_DLL
+#    ifdef ZLIB_INTERNAL
+#      define ZEXPORT   __declspec(dllexport)
+#      define ZEXPORTVA __declspec(dllexport)
+#    else
+#      define ZEXPORT   __declspec(dllimport)
+#      define ZEXPORTVA __declspec(dllimport)
+#    endif
+#  endif
+#endif
+
+#ifndef ZEXTERN
+#  define ZEXTERN extern
+#endif
+#ifndef ZEXPORT
+#  define ZEXPORT
+#endif
+#ifndef ZEXPORTVA
+#  define ZEXPORTVA
+#endif
+
+#ifndef FAR
+#  define FAR
+#endif
+
+#if !defined(__MACTYPES__)
+typedef unsigned char  Byte;  /* 8 bits */
+#endif
+typedef unsigned int   uInt;  /* 16 bits or more */
+typedef unsigned long  uLong; /* 32 bits or more */
+
+#ifdef SMALL_MEDIUM
+   /* Borland C/C++ and some old MSC versions ignore FAR inside typedef */
+#  define Bytef Byte FAR
+#else
+   typedef Byte  FAR Bytef;
+#endif
+typedef char  FAR charf;
+typedef int   FAR intf;
+typedef uInt  FAR uIntf;
+typedef uLong FAR uLongf;
+
+#ifdef STDC
+   typedef void const *voidpc;
+   typedef void FAR   *voidpf;
+   typedef void       *voidp;
+#else
+   typedef Byte const *voidpc;
+   typedef Byte FAR   *voidpf;
+   typedef Byte       *voidp;
+#endif
+
+#if 0           /* HAVE_UNISTD_H -- this line is updated by ./configure */
+#  include <sys/types.h> /* for off_t */
+#  include <unistd.h>    /* for SEEK_* and off_t */
+#  ifdef VMS
+#    include <unixio.h>   /* for off_t */
+#  endif
+#  define z_off_t off_t
+#endif
+#ifndef SEEK_SET
+#  define SEEK_SET        0       /* Seek from beginning of file.  */
+#  define SEEK_CUR        1       /* Seek from current position.  */
+#  define SEEK_END        2       /* Set file pointer to EOF plus "offset" */
+#endif
+#ifndef z_off_t
+#  define z_off_t long
+#endif
+
+#if defined(__OS400__)
+#  define NO_vsnprintf
+#endif
+
+#if defined(__MVS__)
+#  define NO_vsnprintf
+#  ifdef FAR
+#    undef FAR
+#  endif
+#endif
+
+/* MVS linker does not support external names larger than 8 bytes */
+#if defined(__MVS__)
+#   pragma map(deflateInit_,"DEIN")
+#   pragma map(deflateInit2_,"DEIN2")
+#   pragma map(deflateEnd,"DEEND")
+#   pragma map(deflateBound,"DEBND")
+#   pragma map(inflateInit_,"ININ")
+#   pragma map(inflateInit2_,"ININ2")
+#   pragma map(inflateEnd,"INEND")
+#   pragma map(inflateSync,"INSY")
+#   pragma map(inflateSetDictionary,"INSEDI")
+#   pragma map(compressBound,"CMBND")
+#   pragma map(inflate_table,"INTABL")
+#   pragma map(inflate_fast,"INFA")
+#   pragma map(inflate_copyright,"INCOPY")
+#endif
+
+#endif /* ZCONF_H */
diff --git a/3rdp/win32.release/zlib/include/zlib.h b/3rdp/win32.release/zlib/include/zlib.h
new file mode 100644
index 0000000000000000000000000000000000000000..022817927ce3d6b1abe5ac57bff70e7de5291ae0
--- /dev/null
+++ b/3rdp/win32.release/zlib/include/zlib.h
@@ -0,0 +1,1357 @@
+/* zlib.h -- interface of the 'zlib' general purpose compression library
+  version 1.2.3, July 18th, 2005
+
+  Copyright (C) 1995-2005 Jean-loup Gailly and Mark Adler
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+
+  Jean-loup Gailly        Mark Adler
+  jloup@gzip.org          madler@alumni.caltech.edu
+
+
+  The data format used by the zlib library is described by RFCs (Request for
+  Comments) 1950 to 1952 in the files http://www.ietf.org/rfc/rfc1950.txt
+  (zlib format), rfc1951.txt (deflate format) and rfc1952.txt (gzip format).
+*/
+
+#ifndef ZLIB_H
+#define ZLIB_H
+
+#include "zconf.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define ZLIB_VERSION "1.2.3"
+#define ZLIB_VERNUM 0x1230
+
+/*
+     The 'zlib' compression library provides in-memory compression and
+  decompression functions, including integrity checks of the uncompressed
+  data.  This version of the library supports only one compression method
+  (deflation) but other algorithms will be added later and will have the same
+  stream interface.
+
+     Compression can be done in a single step if the buffers are large
+  enough (for example if an input file is mmap'ed), or can be done by
+  repeated calls of the compression function.  In the latter case, the
+  application must provide more input and/or consume the output
+  (providing more output space) before each call.
+
+     The compressed data format used by default by the in-memory functions is
+  the zlib format, which is a zlib wrapper documented in RFC 1950, wrapped
+  around a deflate stream, which is itself documented in RFC 1951.
+
+     The library also supports reading and writing files in gzip (.gz) format
+  with an interface similar to that of stdio using the functions that start
+  with "gz".  The gzip format is different from the zlib format.  gzip is a
+  gzip wrapper, documented in RFC 1952, wrapped around a deflate stream.
+
+     This library can optionally read and write gzip streams in memory as well.
+
+     The zlib format was designed to be compact and fast for use in memory
+  and on communications channels.  The gzip format was designed for single-
+  file compression on file systems, has a larger header than zlib to maintain
+  directory information, and uses a different, slower check method than zlib.
+
+     The library does not install any signal handler. The decoder checks
+  the consistency of the compressed data, so the library should never
+  crash even in case of corrupted input.
+*/
+
+typedef voidpf (*alloc_func) OF((voidpf opaque, uInt items, uInt size));
+typedef void   (*free_func)  OF((voidpf opaque, voidpf address));
+
+struct internal_state;
+
+typedef struct z_stream_s {
+    Bytef    *next_in;  /* next input byte */
+    uInt     avail_in;  /* number of bytes available at next_in */
+    uLong    total_in;  /* total nb of input bytes read so far */
+
+    Bytef    *next_out; /* next output byte should be put there */
+    uInt     avail_out; /* remaining free space at next_out */
+    uLong    total_out; /* total nb of bytes output so far */
+
+    char     *msg;      /* last error message, NULL if no error */
+    struct internal_state FAR *state; /* not visible by applications */
+
+    alloc_func zalloc;  /* used to allocate the internal state */
+    free_func  zfree;   /* used to free the internal state */
+    voidpf     opaque;  /* private data object passed to zalloc and zfree */
+
+    int     data_type;  /* best guess about the data type: binary or text */
+    uLong   adler;      /* adler32 value of the uncompressed data */
+    uLong   reserved;   /* reserved for future use */
+} z_stream;
+
+typedef z_stream FAR *z_streamp;
+
+/*
+     gzip header information passed to and from zlib routines.  See RFC 1952
+  for more details on the meanings of these fields.
+*/
+typedef struct gz_header_s {
+    int     text;       /* true if compressed data believed to be text */
+    uLong   time;       /* modification time */
+    int     xflags;     /* extra flags (not used when writing a gzip file) */
+    int     os;         /* operating system */
+    Bytef   *extra;     /* pointer to extra field or Z_NULL if none */
+    uInt    extra_len;  /* extra field length (valid if extra != Z_NULL) */
+    uInt    extra_max;  /* space at extra (only when reading header) */
+    Bytef   *name;      /* pointer to zero-terminated file name or Z_NULL */
+    uInt    name_max;   /* space at name (only when reading header) */
+    Bytef   *comment;   /* pointer to zero-terminated comment or Z_NULL */
+    uInt    comm_max;   /* space at comment (only when reading header) */
+    int     hcrc;       /* true if there was or will be a header crc */
+    int     done;       /* true when done reading gzip header (not used
+                           when writing a gzip file) */
+} gz_header;
+
+typedef gz_header FAR *gz_headerp;
+
+/*
+   The application must update next_in and avail_in when avail_in has
+   dropped to zero. It must update next_out and avail_out when avail_out
+   has dropped to zero. The application must initialize zalloc, zfree and
+   opaque before calling the init function. All other fields are set by the
+   compression library and must not be updated by the application.
+
+   The opaque value provided by the application will be passed as the first
+   parameter for calls of zalloc and zfree. This can be useful for custom
+   memory management. The compression library attaches no meaning to the
+   opaque value.
+
+   zalloc must return Z_NULL if there is not enough memory for the object.
+   If zlib is used in a multi-threaded application, zalloc and zfree must be
+   thread safe.
+
+   On 16-bit systems, the functions zalloc and zfree must be able to allocate
+   exactly 65536 bytes, but will not be required to allocate more than this
+   if the symbol MAXSEG_64K is defined (see zconf.h). WARNING: On MSDOS,
+   pointers returned by zalloc for objects of exactly 65536 bytes *must*
+   have their offset normalized to zero. The default allocation function
+   provided by this library ensures this (see zutil.c). To reduce memory
+   requirements and avoid any allocation of 64K objects, at the expense of
+   compression ratio, compile the library with -DMAX_WBITS=14 (see zconf.h).
+
+   The fields total_in and total_out can be used for statistics or
+   progress reports. After compression, total_in holds the total size of
+   the uncompressed data and may be saved for use in the decompressor
+   (particularly if the decompressor wants to decompress everything in
+   a single step).
+*/
+
+                        /* constants */
+
+#define Z_NO_FLUSH      0
+#define Z_PARTIAL_FLUSH 1 /* will be removed, use Z_SYNC_FLUSH instead */
+#define Z_SYNC_FLUSH    2
+#define Z_FULL_FLUSH    3
+#define Z_FINISH        4
+#define Z_BLOCK         5
+/* Allowed flush values; see deflate() and inflate() below for details */
+
+#define Z_OK            0
+#define Z_STREAM_END    1
+#define Z_NEED_DICT     2
+#define Z_ERRNO        (-1)
+#define Z_STREAM_ERROR (-2)
+#define Z_DATA_ERROR   (-3)
+#define Z_MEM_ERROR    (-4)
+#define Z_BUF_ERROR    (-5)
+#define Z_VERSION_ERROR (-6)
+/* Return codes for the compression/decompression functions. Negative
+ * values are errors, positive values are used for special but normal events.
+ */
+
+#define Z_NO_COMPRESSION         0
+#define Z_BEST_SPEED             1
+#define Z_BEST_COMPRESSION       9
+#define Z_DEFAULT_COMPRESSION  (-1)
+/* compression levels */
+
+#define Z_FILTERED            1
+#define Z_HUFFMAN_ONLY        2
+#define Z_RLE                 3
+#define Z_FIXED               4
+#define Z_DEFAULT_STRATEGY    0
+/* compression strategy; see deflateInit2() below for details */
+
+#define Z_BINARY   0
+#define Z_TEXT     1
+#define Z_ASCII    Z_TEXT   /* for compatibility with 1.2.2 and earlier */
+#define Z_UNKNOWN  2
+/* Possible values of the data_type field (though see inflate()) */
+
+#define Z_DEFLATED   8
+/* The deflate compression method (the only one supported in this version) */
+
+#define Z_NULL  0  /* for initializing zalloc, zfree, opaque */
+
+#define zlib_version zlibVersion()
+/* for compatibility with versions < 1.0.2 */
+
+                        /* basic functions */
+
+ZEXTERN const char * ZEXPORT zlibVersion OF((void));
+/* The application can compare zlibVersion and ZLIB_VERSION for consistency.
+   If the first character differs, the library code actually used is
+   not compatible with the zlib.h header file used by the application.
+   This check is automatically made by deflateInit and inflateInit.
+ */
+
+/*
+ZEXTERN int ZEXPORT deflateInit OF((z_streamp strm, int level));
+
+     Initializes the internal stream state for compression. The fields
+   zalloc, zfree and opaque must be initialized before by the caller.
+   If zalloc and zfree are set to Z_NULL, deflateInit updates them to
+   use default allocation functions.
+
+     The compression level must be Z_DEFAULT_COMPRESSION, or between 0 and 9:
+   1 gives best speed, 9 gives best compression, 0 gives no compression at
+   all (the input data is simply copied a block at a time).
+   Z_DEFAULT_COMPRESSION requests a default compromise between speed and
+   compression (currently equivalent to level 6).
+
+     deflateInit returns Z_OK if success, Z_MEM_ERROR if there was not
+   enough memory, Z_STREAM_ERROR if level is not a valid compression level,
+   Z_VERSION_ERROR if the zlib library version (zlib_version) is incompatible
+   with the version assumed by the caller (ZLIB_VERSION).
+   msg is set to null if there is no error message.  deflateInit does not
+   perform any compression: this will be done by deflate().
+*/
+
+
+ZEXTERN int ZEXPORT deflate OF((z_streamp strm, int flush));
+/*
+    deflate compresses as much data as possible, and stops when the input
+  buffer becomes empty or the output buffer becomes full. It may introduce some
+  output latency (reading input without producing any output) except when
+  forced to flush.
+
+    The detailed semantics are as follows. deflate performs one or both of the
+  following actions:
+
+  - Compress more input starting at next_in and update next_in and avail_in
+    accordingly. If not all input can be processed (because there is not
+    enough room in the output buffer), next_in and avail_in are updated and
+    processing will resume at this point for the next call of deflate().
+
+  - Provide more output starting at next_out and update next_out and avail_out
+    accordingly. This action is forced if the parameter flush is non zero.
+    Forcing flush frequently degrades the compression ratio, so this parameter
+    should be set only when necessary (in interactive applications).
+    Some output may be provided even if flush is not set.
+
+  Before the call of deflate(), the application should ensure that at least
+  one of the actions is possible, by providing more input and/or consuming
+  more output, and updating avail_in or avail_out accordingly; avail_out
+  should never be zero before the call. The application can consume the
+  compressed output when it wants, for example when the output buffer is full
+  (avail_out == 0), or after each call of deflate(). If deflate returns Z_OK
+  and with zero avail_out, it must be called again after making room in the
+  output buffer because there might be more output pending.
+
+    Normally the parameter flush is set to Z_NO_FLUSH, which allows deflate to
+  decide how much data to accumualte before producing output, in order to
+  maximize compression.
+
+    If the parameter flush is set to Z_SYNC_FLUSH, all pending output is
+  flushed to the output buffer and the output is aligned on a byte boundary, so
+  that the decompressor can get all input data available so far. (In particular
+  avail_in is zero after the call if enough output space has been provided
+  before the call.)  Flushing may degrade compression for some compression
+  algorithms and so it should be used only when necessary.
+
+    If flush is set to Z_FULL_FLUSH, all output is flushed as with
+  Z_SYNC_FLUSH, and the compression state is reset so that decompression can
+  restart from this point if previous compressed data has been damaged or if
+  random access is desired. Using Z_FULL_FLUSH too often can seriously degrade
+  compression.
+
+    If deflate returns with avail_out == 0, this function must be called again
+  with the same value of the flush parameter and more output space (updated
+  avail_out), until the flush is complete (deflate returns with non-zero
+  avail_out). In the case of a Z_FULL_FLUSH or Z_SYNC_FLUSH, make sure that
+  avail_out is greater than six to avoid repeated flush markers due to
+  avail_out == 0 on return.
+
+    If the parameter flush is set to Z_FINISH, pending input is processed,
+  pending output is flushed and deflate returns with Z_STREAM_END if there
+  was enough output space; if deflate returns with Z_OK, this function must be
+  called again with Z_FINISH and more output space (updated avail_out) but no
+  more input data, until it returns with Z_STREAM_END or an error. After
+  deflate has returned Z_STREAM_END, the only possible operations on the
+  stream are deflateReset or deflateEnd.
+
+    Z_FINISH can be used immediately after deflateInit if all the compression
+  is to be done in a single step. In this case, avail_out must be at least
+  the value returned by deflateBound (see below). If deflate does not return
+  Z_STREAM_END, then it must be called again as described above.
+
+    deflate() sets strm->adler to the adler32 checksum of all input read
+  so far (that is, total_in bytes).
+
+    deflate() may update strm->data_type if it can make a good guess about
+  the input data type (Z_BINARY or Z_TEXT). In doubt, the data is considered
+  binary. This field is only for information purposes and does not affect
+  the compression algorithm in any manner.
+
+    deflate() returns Z_OK if some progress has been made (more input
+  processed or more output produced), Z_STREAM_END if all input has been
+  consumed and all output has been produced (only when flush is set to
+  Z_FINISH), Z_STREAM_ERROR if the stream state was inconsistent (for example
+  if next_in or next_out was NULL), Z_BUF_ERROR if no progress is possible
+  (for example avail_in or avail_out was zero). Note that Z_BUF_ERROR is not
+  fatal, and deflate() can be called again with more input and more output
+  space to continue compressing.
+*/
+
+
+ZEXTERN int ZEXPORT deflateEnd OF((z_streamp strm));
+/*
+     All dynamically allocated data structures for this stream are freed.
+   This function discards any unprocessed input and does not flush any
+   pending output.
+
+     deflateEnd returns Z_OK if success, Z_STREAM_ERROR if the
+   stream state was inconsistent, Z_DATA_ERROR if the stream was freed
+   prematurely (some input or output was discarded). In the error case,
+   msg may be set but then points to a static string (which must not be
+   deallocated).
+*/
+
+
+/*
+ZEXTERN int ZEXPORT inflateInit OF((z_streamp strm));
+
+     Initializes the internal stream state for decompression. The fields
+   next_in, avail_in, zalloc, zfree and opaque must be initialized before by
+   the caller. If next_in is not Z_NULL and avail_in is large enough (the exact
+   value depends on the compression method), inflateInit determines the
+   compression method from the zlib header and allocates all data structures
+   accordingly; otherwise the allocation will be deferred to the first call of
+   inflate.  If zalloc and zfree are set to Z_NULL, inflateInit updates them to
+   use default allocation functions.
+
+     inflateInit returns Z_OK if success, Z_MEM_ERROR if there was not enough
+   memory, Z_VERSION_ERROR if the zlib library version is incompatible with the
+   version assumed by the caller.  msg is set to null if there is no error
+   message. inflateInit does not perform any decompression apart from reading
+   the zlib header if present: this will be done by inflate().  (So next_in and
+   avail_in may be modified, but next_out and avail_out are unchanged.)
+*/
+
+
+ZEXTERN int ZEXPORT inflate OF((z_streamp strm, int flush));
+/*
+    inflate decompresses as much data as possible, and stops when the input
+  buffer becomes empty or the output buffer becomes full. It may introduce
+  some output latency (reading input without producing any output) except when
+  forced to flush.
+
+  The detailed semantics are as follows. inflate performs one or both of the
+  following actions:
+
+  - Decompress more input starting at next_in and update next_in and avail_in
+    accordingly. If not all input can be processed (because there is not
+    enough room in the output buffer), next_in is updated and processing
+    will resume at this point for the next call of inflate().
+
+  - Provide more output starting at next_out and update next_out and avail_out
+    accordingly.  inflate() provides as much output as possible, until there
+    is no more input data or no more space in the output buffer (see below
+    about the flush parameter).
+
+  Before the call of inflate(), the application should ensure that at least
+  one of the actions is possible, by providing more input and/or consuming
+  more output, and updating the next_* and avail_* values accordingly.
+  The application can consume the uncompressed output when it wants, for
+  example when the output buffer is full (avail_out == 0), or after each
+  call of inflate(). If inflate returns Z_OK and with zero avail_out, it
+  must be called again after making room in the output buffer because there
+  might be more output pending.
+
+    The flush parameter of inflate() can be Z_NO_FLUSH, Z_SYNC_FLUSH,
+  Z_FINISH, or Z_BLOCK. Z_SYNC_FLUSH requests that inflate() flush as much
+  output as possible to the output buffer. Z_BLOCK requests that inflate() stop
+  if and when it gets to the next deflate block boundary. When decoding the
+  zlib or gzip format, this will cause inflate() to return immediately after
+  the header and before the first block. When doing a raw inflate, inflate()
+  will go ahead and process the first block, and will return when it gets to
+  the end of that block, or when it runs out of data.
+
+    The Z_BLOCK option assists in appending to or combining deflate streams.
+  Also to assist in this, on return inflate() will set strm->data_type to the
+  number of unused bits in the last byte taken from strm->next_in, plus 64
+  if inflate() is currently decoding the last block in the deflate stream,
+  plus 128 if inflate() returned immediately after decoding an end-of-block
+  code or decoding the complete header up to just before the first byte of the
+  deflate stream. The end-of-block will not be indicated until all of the
+  uncompressed data from that block has been written to strm->next_out.  The
+  number of unused bits may in general be greater than seven, except when
+  bit 7 of data_type is set, in which case the number of unused bits will be
+  less than eight.
+
+    inflate() should normally be called until it returns Z_STREAM_END or an
+  error. However if all decompression is to be performed in a single step
+  (a single call of inflate), the parameter flush should be set to
+  Z_FINISH. In this case all pending input is processed and all pending
+  output is flushed; avail_out must be large enough to hold all the
+  uncompressed data. (The size of the uncompressed data may have been saved
+  by the compressor for this purpose.) The next operation on this stream must
+  be inflateEnd to deallocate the decompression state. The use of Z_FINISH
+  is never required, but can be used to inform inflate that a faster approach
+  may be used for the single inflate() call.
+
+     In this implementation, inflate() always flushes as much output as
+  possible to the output buffer, and always uses the faster approach on the
+  first call. So the only effect of the flush parameter in this implementation
+  is on the return value of inflate(), as noted below, or when it returns early
+  because Z_BLOCK is used.
+
+     If a preset dictionary is needed after this call (see inflateSetDictionary
+  below), inflate sets strm->adler to the adler32 checksum of the dictionary
+  chosen by the compressor and returns Z_NEED_DICT; otherwise it sets
+  strm->adler to the adler32 checksum of all output produced so far (that is,
+  total_out bytes) and returns Z_OK, Z_STREAM_END or an error code as described
+  below. At the end of the stream, inflate() checks that its computed adler32
+  checksum is equal to that saved by the compressor and returns Z_STREAM_END
+  only if the checksum is correct.
+
+    inflate() will decompress and check either zlib-wrapped or gzip-wrapped
+  deflate data.  The header type is detected automatically.  Any information
+  contained in the gzip header is not retained, so applications that need that
+  information should instead use raw inflate, see inflateInit2() below, or
+  inflateBack() and perform their own processing of the gzip header and
+  trailer.
+
+    inflate() returns Z_OK if some progress has been made (more input processed
+  or more output produced), Z_STREAM_END if the end of the compressed data has
+  been reached and all uncompressed output has been produced, Z_NEED_DICT if a
+  preset dictionary is needed at this point, Z_DATA_ERROR if the input data was
+  corrupted (input stream not conforming to the zlib format or incorrect check
+  value), Z_STREAM_ERROR if the stream structure was inconsistent (for example
+  if next_in or next_out was NULL), Z_MEM_ERROR if there was not enough memory,
+  Z_BUF_ERROR if no progress is possible or if there was not enough room in the
+  output buffer when Z_FINISH is used. Note that Z_BUF_ERROR is not fatal, and
+  inflate() can be called again with more input and more output space to
+  continue decompressing. If Z_DATA_ERROR is returned, the application may then
+  call inflateSync() to look for a good compression block if a partial recovery
+  of the data is desired.
+*/
+
+
+ZEXTERN int ZEXPORT inflateEnd OF((z_streamp strm));
+/*
+     All dynamically allocated data structures for this stream are freed.
+   This function discards any unprocessed input and does not flush any
+   pending output.
+
+     inflateEnd returns Z_OK if success, Z_STREAM_ERROR if the stream state
+   was inconsistent. In the error case, msg may be set but then points to a
+   static string (which must not be deallocated).
+*/
+
+                        /* Advanced functions */
+
+/*
+    The following functions are needed only in some special applications.
+*/
+
+/*
+ZEXTERN int ZEXPORT deflateInit2 OF((z_streamp strm,
+                                     int  level,
+                                     int  method,
+                                     int  windowBits,
+                                     int  memLevel,
+                                     int  strategy));
+
+     This is another version of deflateInit with more compression options. The
+   fields next_in, zalloc, zfree and opaque must be initialized before by
+   the caller.
+
+     The method parameter is the compression method. It must be Z_DEFLATED in
+   this version of the library.
+
+     The windowBits parameter is the base two logarithm of the window size
+   (the size of the history buffer). It should be in the range 8..15 for this
+   version of the library. Larger values of this parameter result in better
+   compression at the expense of memory usage. The default value is 15 if
+   deflateInit is used instead.
+
+     windowBits can also be -8..-15 for raw deflate. In this case, -windowBits
+   determines the window size. deflate() will then generate raw deflate data
+   with no zlib header or trailer, and will not compute an adler32 check value.
+
+     windowBits can also be greater than 15 for optional gzip encoding. Add
+   16 to windowBits to write a simple gzip header and trailer around the
+   compressed data instead of a zlib wrapper. The gzip header will have no
+   file name, no extra data, no comment, no modification time (set to zero),
+   no header crc, and the operating system will be set to 255 (unknown).  If a
+   gzip stream is being written, strm->adler is a crc32 instead of an adler32.
+
+     The memLevel parameter specifies how much memory should be allocated
+   for the internal compression state. memLevel=1 uses minimum memory but
+   is slow and reduces compression ratio; memLevel=9 uses maximum memory
+   for optimal speed. The default value is 8. See zconf.h for total memory
+   usage as a function of windowBits and memLevel.
+
+     The strategy parameter is used to tune the compression algorithm. Use the
+   value Z_DEFAULT_STRATEGY for normal data, Z_FILTERED for data produced by a
+   filter (or predictor), Z_HUFFMAN_ONLY to force Huffman encoding only (no
+   string match), or Z_RLE to limit match distances to one (run-length
+   encoding). Filtered data consists mostly of small values with a somewhat
+   random distribution. In this case, the compression algorithm is tuned to
+   compress them better. The effect of Z_FILTERED is to force more Huffman
+   coding and less string matching; it is somewhat intermediate between
+   Z_DEFAULT and Z_HUFFMAN_ONLY. Z_RLE is designed to be almost as fast as
+   Z_HUFFMAN_ONLY, but give better compression for PNG image data. The strategy
+   parameter only affects the compression ratio but not the correctness of the
+   compressed output even if it is not set appropriately.  Z_FIXED prevents the
+   use of dynamic Huffman codes, allowing for a simpler decoder for special
+   applications.
+
+      deflateInit2 returns Z_OK if success, Z_MEM_ERROR if there was not enough
+   memory, Z_STREAM_ERROR if a parameter is invalid (such as an invalid
+   method). msg is set to null if there is no error message.  deflateInit2 does
+   not perform any compression: this will be done by deflate().
+*/
+
+ZEXTERN int ZEXPORT deflateSetDictionary OF((z_streamp strm,
+                                             const Bytef *dictionary,
+                                             uInt  dictLength));
+/*
+     Initializes the compression dictionary from the given byte sequence
+   without producing any compressed output. This function must be called
+   immediately after deflateInit, deflateInit2 or deflateReset, before any
+   call of deflate. The compressor and decompressor must use exactly the same
+   dictionary (see inflateSetDictionary).
+
+     The dictionary should consist of strings (byte sequences) that are likely
+   to be encountered later in the data to be compressed, with the most commonly
+   used strings preferably put towards the end of the dictionary. Using a
+   dictionary is most useful when the data to be compressed is short and can be
+   predicted with good accuracy; the data can then be compressed better than
+   with the default empty dictionary.
+
+     Depending on the size of the compression data structures selected by
+   deflateInit or deflateInit2, a part of the dictionary may in effect be
+   discarded, for example if the dictionary is larger than the window size in
+   deflate or deflate2. Thus the strings most likely to be useful should be
+   put at the end of the dictionary, not at the front. In addition, the
+   current implementation of deflate will use at most the window size minus
+   262 bytes of the provided dictionary.
+
+     Upon return of this function, strm->adler is set to the adler32 value
+   of the dictionary; the decompressor may later use this value to determine
+   which dictionary has been used by the compressor. (The adler32 value
+   applies to the whole dictionary even if only a subset of the dictionary is
+   actually used by the compressor.) If a raw deflate was requested, then the
+   adler32 value is not computed and strm->adler is not set.
+
+     deflateSetDictionary returns Z_OK if success, or Z_STREAM_ERROR if a
+   parameter is invalid (such as NULL dictionary) or the stream state is
+   inconsistent (for example if deflate has already been called for this stream
+   or if the compression method is bsort). deflateSetDictionary does not
+   perform any compression: this will be done by deflate().
+*/
+
+ZEXTERN int ZEXPORT deflateCopy OF((z_streamp dest,
+                                    z_streamp source));
+/*
+     Sets the destination stream as a complete copy of the source stream.
+
+     This function can be useful when several compression strategies will be
+   tried, for example when there are several ways of pre-processing the input
+   data with a filter. The streams that will be discarded should then be freed
+   by calling deflateEnd.  Note that deflateCopy duplicates the internal
+   compression state which can be quite large, so this strategy is slow and
+   can consume lots of memory.
+
+     deflateCopy returns Z_OK if success, Z_MEM_ERROR if there was not
+   enough memory, Z_STREAM_ERROR if the source stream state was inconsistent
+   (such as zalloc being NULL). msg is left unchanged in both source and
+   destination.
+*/
+
+ZEXTERN int ZEXPORT deflateReset OF((z_streamp strm));
+/*
+     This function is equivalent to deflateEnd followed by deflateInit,
+   but does not free and reallocate all the internal compression state.
+   The stream will keep the same compression level and any other attributes
+   that may have been set by deflateInit2.
+
+      deflateReset returns Z_OK if success, or Z_STREAM_ERROR if the source
+   stream state was inconsistent (such as zalloc or state being NULL).
+*/
+
+ZEXTERN int ZEXPORT deflateParams OF((z_streamp strm,
+                                      int level,
+                                      int strategy));
+/*
+     Dynamically update the compression level and compression strategy.  The
+   interpretation of level and strategy is as in deflateInit2.  This can be
+   used to switch between compression and straight copy of the input data, or
+   to switch to a different kind of input data requiring a different
+   strategy. If the compression level is changed, the input available so far
+   is compressed with the old level (and may be flushed); the new level will
+   take effect only at the next call of deflate().
+
+     Before the call of deflateParams, the stream state must be set as for
+   a call of deflate(), since the currently available input may have to
+   be compressed and flushed. In particular, strm->avail_out must be non-zero.
+
+     deflateParams returns Z_OK if success, Z_STREAM_ERROR if the source
+   stream state was inconsistent or if a parameter was invalid, Z_BUF_ERROR
+   if strm->avail_out was zero.
+*/
+
+ZEXTERN int ZEXPORT deflateTune OF((z_streamp strm,
+                                    int good_length,
+                                    int max_lazy,
+                                    int nice_length,
+                                    int max_chain));
+/*
+     Fine tune deflate's internal compression parameters.  This should only be
+   used by someone who understands the algorithm used by zlib's deflate for
+   searching for the best matching string, and even then only by the most
+   fanatic optimizer trying to squeeze out the last compressed bit for their
+   specific input data.  Read the deflate.c source code for the meaning of the
+   max_lazy, good_length, nice_length, and max_chain parameters.
+
+     deflateTune() can be called after deflateInit() or deflateInit2(), and
+   returns Z_OK on success, or Z_STREAM_ERROR for an invalid deflate stream.
+ */
+
+ZEXTERN uLong ZEXPORT deflateBound OF((z_streamp strm,
+                                       uLong sourceLen));
+/*
+     deflateBound() returns an upper bound on the compressed size after
+   deflation of sourceLen bytes.  It must be called after deflateInit()
+   or deflateInit2().  This would be used to allocate an output buffer
+   for deflation in a single pass, and so would be called before deflate().
+*/
+
+ZEXTERN int ZEXPORT deflatePrime OF((z_streamp strm,
+                                     int bits,
+                                     int value));
+/*
+     deflatePrime() inserts bits in the deflate output stream.  The intent
+  is that this function is used to start off the deflate output with the
+  bits leftover from a previous deflate stream when appending to it.  As such,
+  this function can only be used for raw deflate, and must be used before the
+  first deflate() call after a deflateInit2() or deflateReset().  bits must be
+  less than or equal to 16, and that many of the least significant bits of
+  value will be inserted in the output.
+
+      deflatePrime returns Z_OK if success, or Z_STREAM_ERROR if the source
+   stream state was inconsistent.
+*/
+
+ZEXTERN int ZEXPORT deflateSetHeader OF((z_streamp strm,
+                                         gz_headerp head));
+/*
+      deflateSetHeader() provides gzip header information for when a gzip
+   stream is requested by deflateInit2().  deflateSetHeader() may be called
+   after deflateInit2() or deflateReset() and before the first call of
+   deflate().  The text, time, os, extra field, name, and comment information
+   in the provided gz_header structure are written to the gzip header (xflag is
+   ignored -- the extra flags are set according to the compression level).  The
+   caller must assure that, if not Z_NULL, name and comment are terminated with
+   a zero byte, and that if extra is not Z_NULL, that extra_len bytes are
+   available there.  If hcrc is true, a gzip header crc is included.  Note that
+   the current versions of the command-line version of gzip (up through version
+   1.3.x) do not support header crc's, and will report that it is a "multi-part
+   gzip file" and give up.
+
+      If deflateSetHeader is not used, the default gzip header has text false,
+   the time set to zero, and os set to 255, with no extra, name, or comment
+   fields.  The gzip header is returned to the default state by deflateReset().
+
+      deflateSetHeader returns Z_OK if success, or Z_STREAM_ERROR if the source
+   stream state was inconsistent.
+*/
+
+/*
+ZEXTERN int ZEXPORT inflateInit2 OF((z_streamp strm,
+                                     int  windowBits));
+
+     This is another version of inflateInit with an extra parameter. The
+   fields next_in, avail_in, zalloc, zfree and opaque must be initialized
+   before by the caller.
+
+     The windowBits parameter is the base two logarithm of the maximum window
+   size (the size of the history buffer).  It should be in the range 8..15 for
+   this version of the library. The default value is 15 if inflateInit is used
+   instead. windowBits must be greater than or equal to the windowBits value
+   provided to deflateInit2() while compressing, or it must be equal to 15 if
+   deflateInit2() was not used. If a compressed stream with a larger window
+   size is given as input, inflate() will return with the error code
+   Z_DATA_ERROR instead of trying to allocate a larger window.
+
+     windowBits can also be -8..-15 for raw inflate. In this case, -windowBits
+   determines the window size. inflate() will then process raw deflate data,
+   not looking for a zlib or gzip header, not generating a check value, and not
+   looking for any check values for comparison at the end of the stream. This
+   is for use with other formats that use the deflate compressed data format
+   such as zip.  Those formats provide their own check values. If a custom
+   format is developed using the raw deflate format for compressed data, it is
+   recommended that a check value such as an adler32 or a crc32 be applied to
+   the uncompressed data as is done in the zlib, gzip, and zip formats.  For
+   most applications, the zlib format should be used as is. Note that comments
+   above on the use in deflateInit2() applies to the magnitude of windowBits.
+
+     windowBits can also be greater than 15 for optional gzip decoding. Add
+   32 to windowBits to enable zlib and gzip decoding with automatic header
+   detection, or add 16 to decode only the gzip format (the zlib format will
+   return a Z_DATA_ERROR).  If a gzip stream is being decoded, strm->adler is
+   a crc32 instead of an adler32.
+
+     inflateInit2 returns Z_OK if success, Z_MEM_ERROR if there was not enough
+   memory, Z_STREAM_ERROR if a parameter is invalid (such as a null strm). msg
+   is set to null if there is no error message.  inflateInit2 does not perform
+   any decompression apart from reading the zlib header if present: this will
+   be done by inflate(). (So next_in and avail_in may be modified, but next_out
+   and avail_out are unchanged.)
+*/
+
+ZEXTERN int ZEXPORT inflateSetDictionary OF((z_streamp strm,
+                                             const Bytef *dictionary,
+                                             uInt  dictLength));
+/*
+     Initializes the decompression dictionary from the given uncompressed byte
+   sequence. This function must be called immediately after a call of inflate,
+   if that call returned Z_NEED_DICT. The dictionary chosen by the compressor
+   can be determined from the adler32 value returned by that call of inflate.
+   The compressor and decompressor must use exactly the same dictionary (see
+   deflateSetDictionary).  For raw inflate, this function can be called
+   immediately after inflateInit2() or inflateReset() and before any call of
+   inflate() to set the dictionary.  The application must insure that the
+   dictionary that was used for compression is provided.
+
+     inflateSetDictionary returns Z_OK if success, Z_STREAM_ERROR if a
+   parameter is invalid (such as NULL dictionary) or the stream state is
+   inconsistent, Z_DATA_ERROR if the given dictionary doesn't match the
+   expected one (incorrect adler32 value). inflateSetDictionary does not
+   perform any decompression: this will be done by subsequent calls of
+   inflate().
+*/
+
+ZEXTERN int ZEXPORT inflateSync OF((z_streamp strm));
+/*
+    Skips invalid compressed data until a full flush point (see above the
+  description of deflate with Z_FULL_FLUSH) can be found, or until all
+  available input is skipped. No output is provided.
+
+    inflateSync returns Z_OK if a full flush point has been found, Z_BUF_ERROR
+  if no more input was provided, Z_DATA_ERROR if no flush point has been found,
+  or Z_STREAM_ERROR if the stream structure was inconsistent. In the success
+  case, the application may save the current current value of total_in which
+  indicates where valid compressed data was found. In the error case, the
+  application may repeatedly call inflateSync, providing more input each time,
+  until success or end of the input data.
+*/
+
+ZEXTERN int ZEXPORT inflateCopy OF((z_streamp dest,
+                                    z_streamp source));
+/*
+     Sets the destination stream as a complete copy of the source stream.
+
+     This function can be useful when randomly accessing a large stream.  The
+   first pass through the stream can periodically record the inflate state,
+   allowing restarting inflate at those points when randomly accessing the
+   stream.
+
+     inflateCopy returns Z_OK if success, Z_MEM_ERROR if there was not
+   enough memory, Z_STREAM_ERROR if the source stream state was inconsistent
+   (such as zalloc being NULL). msg is left unchanged in both source and
+   destination.
+*/
+
+ZEXTERN int ZEXPORT inflateReset OF((z_streamp strm));
+/*
+     This function is equivalent to inflateEnd followed by inflateInit,
+   but does not free and reallocate all the internal decompression state.
+   The stream will keep attributes that may have been set by inflateInit2.
+
+      inflateReset returns Z_OK if success, or Z_STREAM_ERROR if the source
+   stream state was inconsistent (such as zalloc or state being NULL).
+*/
+
+ZEXTERN int ZEXPORT inflatePrime OF((z_streamp strm,
+                                     int bits,
+                                     int value));
+/*
+     This function inserts bits in the inflate input stream.  The intent is
+  that this function is used to start inflating at a bit position in the
+  middle of a byte.  The provided bits will be used before any bytes are used
+  from next_in.  This function should only be used with raw inflate, and
+  should be used before the first inflate() call after inflateInit2() or
+  inflateReset().  bits must be less than or equal to 16, and that many of the
+  least significant bits of value will be inserted in the input.
+
+      inflatePrime returns Z_OK if success, or Z_STREAM_ERROR if the source
+   stream state was inconsistent.
+*/
+
+ZEXTERN int ZEXPORT inflateGetHeader OF((z_streamp strm,
+                                         gz_headerp head));
+/*
+      inflateGetHeader() requests that gzip header information be stored in the
+   provided gz_header structure.  inflateGetHeader() may be called after
+   inflateInit2() or inflateReset(), and before the first call of inflate().
+   As inflate() processes the gzip stream, head->done is zero until the header
+   is completed, at which time head->done is set to one.  If a zlib stream is
+   being decoded, then head->done is set to -1 to indicate that there will be
+   no gzip header information forthcoming.  Note that Z_BLOCK can be used to
+   force inflate() to return immediately after header processing is complete
+   and before any actual data is decompressed.
+
+      The text, time, xflags, and os fields are filled in with the gzip header
+   contents.  hcrc is set to true if there is a header CRC.  (The header CRC
+   was valid if done is set to one.)  If extra is not Z_NULL, then extra_max
+   contains the maximum number of bytes to write to extra.  Once done is true,
+   extra_len contains the actual extra field length, and extra contains the
+   extra field, or that field truncated if extra_max is less than extra_len.
+   If name is not Z_NULL, then up to name_max characters are written there,
+   terminated with a zero unless the length is greater than name_max.  If
+   comment is not Z_NULL, then up to comm_max characters are written there,
+   terminated with a zero unless the length is greater than comm_max.  When
+   any of extra, name, or comment are not Z_NULL and the respective field is
+   not present in the header, then that field is set to Z_NULL to signal its
+   absence.  This allows the use of deflateSetHeader() with the returned
+   structure to duplicate the header.  However if those fields are set to
+   allocated memory, then the application will need to save those pointers
+   elsewhere so that they can be eventually freed.
+
+      If inflateGetHeader is not used, then the header information is simply
+   discarded.  The header is always checked for validity, including the header
+   CRC if present.  inflateReset() will reset the process to discard the header
+   information.  The application would need to call inflateGetHeader() again to
+   retrieve the header from the next gzip stream.
+
+      inflateGetHeader returns Z_OK if success, or Z_STREAM_ERROR if the source
+   stream state was inconsistent.
+*/
+
+/*
+ZEXTERN int ZEXPORT inflateBackInit OF((z_streamp strm, int windowBits,
+                                        unsigned char FAR *window));
+
+     Initialize the internal stream state for decompression using inflateBack()
+   calls.  The fields zalloc, zfree and opaque in strm must be initialized
+   before the call.  If zalloc and zfree are Z_NULL, then the default library-
+   derived memory allocation routines are used.  windowBits is the base two
+   logarithm of the window size, in the range 8..15.  window is a caller
+   supplied buffer of that size.  Except for special applications where it is
+   assured that deflate was used with small window sizes, windowBits must be 15
+   and a 32K byte window must be supplied to be able to decompress general
+   deflate streams.
+
+     See inflateBack() for the usage of these routines.
+
+     inflateBackInit will return Z_OK on success, Z_STREAM_ERROR if any of
+   the paramaters are invalid, Z_MEM_ERROR if the internal state could not
+   be allocated, or Z_VERSION_ERROR if the version of the library does not
+   match the version of the header file.
+*/
+
+typedef unsigned (*in_func) OF((void FAR *, unsigned char FAR * FAR *));
+typedef int (*out_func) OF((void FAR *, unsigned char FAR *, unsigned));
+
+ZEXTERN int ZEXPORT inflateBack OF((z_streamp strm,
+                                    in_func in, void FAR *in_desc,
+                                    out_func out, void FAR *out_desc));
+/*
+     inflateBack() does a raw inflate with a single call using a call-back
+   interface for input and output.  This is more efficient than inflate() for
+   file i/o applications in that it avoids copying between the output and the
+   sliding window by simply making the window itself the output buffer.  This
+   function trusts the application to not change the output buffer passed by
+   the output function, at least until inflateBack() returns.
+
+     inflateBackInit() must be called first to allocate the internal state
+   and to initialize the state with the user-provided window buffer.
+   inflateBack() may then be used multiple times to inflate a complete, raw
+   deflate stream with each call.  inflateBackEnd() is then called to free
+   the allocated state.
+
+     A raw deflate stream is one with no zlib or gzip header or trailer.
+   This routine would normally be used in a utility that reads zip or gzip
+   files and writes out uncompressed files.  The utility would decode the
+   header and process the trailer on its own, hence this routine expects
+   only the raw deflate stream to decompress.  This is different from the
+   normal behavior of inflate(), which expects either a zlib or gzip header and
+   trailer around the deflate stream.
+
+     inflateBack() uses two subroutines supplied by the caller that are then
+   called by inflateBack() for input and output.  inflateBack() calls those
+   routines until it reads a complete deflate stream and writes out all of the
+   uncompressed data, or until it encounters an error.  The function's
+   parameters and return types are defined above in the in_func and out_func
+   typedefs.  inflateBack() will call in(in_desc, &buf) which should return the
+   number of bytes of provided input, and a pointer to that input in buf.  If
+   there is no input available, in() must return zero--buf is ignored in that
+   case--and inflateBack() will return a buffer error.  inflateBack() will call
+   out(out_desc, buf, len) to write the uncompressed data buf[0..len-1].  out()
+   should return zero on success, or non-zero on failure.  If out() returns
+   non-zero, inflateBack() will return with an error.  Neither in() nor out()
+   are permitted to change the contents of the window provided to
+   inflateBackInit(), which is also the buffer that out() uses to write from.
+   The length written by out() will be at most the window size.  Any non-zero
+   amount of input may be provided by in().
+
+     For convenience, inflateBack() can be provided input on the first call by
+   setting strm->next_in and strm->avail_in.  If that input is exhausted, then
+   in() will be called.  Therefore strm->next_in must be initialized before
+   calling inflateBack().  If strm->next_in is Z_NULL, then in() will be called
+   immediately for input.  If strm->next_in is not Z_NULL, then strm->avail_in
+   must also be initialized, and then if strm->avail_in is not zero, input will
+   initially be taken from strm->next_in[0 .. strm->avail_in - 1].
+
+     The in_desc and out_desc parameters of inflateBack() is passed as the
+   first parameter of in() and out() respectively when they are called.  These
+   descriptors can be optionally used to pass any information that the caller-
+   supplied in() and out() functions need to do their job.
+
+     On return, inflateBack() will set strm->next_in and strm->avail_in to
+   pass back any unused input that was provided by the last in() call.  The
+   return values of inflateBack() can be Z_STREAM_END on success, Z_BUF_ERROR
+   if in() or out() returned an error, Z_DATA_ERROR if there was a format
+   error in the deflate stream (in which case strm->msg is set to indicate the
+   nature of the error), or Z_STREAM_ERROR if the stream was not properly
+   initialized.  In the case of Z_BUF_ERROR, an input or output error can be
+   distinguished using strm->next_in which will be Z_NULL only if in() returned
+   an error.  If strm->next is not Z_NULL, then the Z_BUF_ERROR was due to
+   out() returning non-zero.  (in() will always be called before out(), so
+   strm->next_in is assured to be defined if out() returns non-zero.)  Note
+   that inflateBack() cannot return Z_OK.
+*/
+
+ZEXTERN int ZEXPORT inflateBackEnd OF((z_streamp strm));
+/*
+     All memory allocated by inflateBackInit() is freed.
+
+     inflateBackEnd() returns Z_OK on success, or Z_STREAM_ERROR if the stream
+   state was inconsistent.
+*/
+
+ZEXTERN uLong ZEXPORT zlibCompileFlags OF((void));
+/* Return flags indicating compile-time options.
+
+    Type sizes, two bits each, 00 = 16 bits, 01 = 32, 10 = 64, 11 = other:
+     1.0: size of uInt
+     3.2: size of uLong
+     5.4: size of voidpf (pointer)
+     7.6: size of z_off_t
+
+    Compiler, assembler, and debug options:
+     8: DEBUG
+     9: ASMV or ASMINF -- use ASM code
+     10: ZLIB_WINAPI -- exported functions use the WINAPI calling convention
+     11: 0 (reserved)
+
+    One-time table building (smaller code, but not thread-safe if true):
+     12: BUILDFIXED -- build static block decoding tables when needed
+     13: DYNAMIC_CRC_TABLE -- build CRC calculation tables when needed
+     14,15: 0 (reserved)
+
+    Library content (indicates missing functionality):
+     16: NO_GZCOMPRESS -- gz* functions cannot compress (to avoid linking
+                          deflate code when not needed)
+     17: NO_GZIP -- deflate can't write gzip streams, and inflate can't detect
+                    and decode gzip streams (to avoid linking crc code)
+     18-19: 0 (reserved)
+
+    Operation variations (changes in library functionality):
+     20: PKZIP_BUG_WORKAROUND -- slightly more permissive inflate
+     21: FASTEST -- deflate algorithm with only one, lowest compression level
+     22,23: 0 (reserved)
+
+    The sprintf variant used by gzprintf (zero is best):
+     24: 0 = vs*, 1 = s* -- 1 means limited to 20 arguments after the format
+     25: 0 = *nprintf, 1 = *printf -- 1 means gzprintf() not secure!
+     26: 0 = returns value, 1 = void -- 1 means inferred string length returned
+
+    Remainder:
+     27-31: 0 (reserved)
+ */
+
+
+                        /* utility functions */
+
+/*
+     The following utility functions are implemented on top of the
+   basic stream-oriented functions. To simplify the interface, some
+   default options are assumed (compression level and memory usage,
+   standard memory allocation functions). The source code of these
+   utility functions can easily be modified if you need special options.
+*/
+
+ZEXTERN int ZEXPORT compress OF((Bytef *dest,   uLongf *destLen,
+                                 const Bytef *source, uLong sourceLen));
+/*
+     Compresses the source buffer into the destination buffer.  sourceLen is
+   the byte length of the source buffer. Upon entry, destLen is the total
+   size of the destination buffer, which must be at least the value returned
+   by compressBound(sourceLen). Upon exit, destLen is the actual size of the
+   compressed buffer.
+     This function can be used to compress a whole file at once if the
+   input file is mmap'ed.
+     compress returns Z_OK if success, Z_MEM_ERROR if there was not
+   enough memory, Z_BUF_ERROR if there was not enough room in the output
+   buffer.
+*/
+
+ZEXTERN int ZEXPORT compress2 OF((Bytef *dest,   uLongf *destLen,
+                                  const Bytef *source, uLong sourceLen,
+                                  int level));
+/*
+     Compresses the source buffer into the destination buffer. The level
+   parameter has the same meaning as in deflateInit.  sourceLen is the byte
+   length of the source buffer. Upon entry, destLen is the total size of the
+   destination buffer, which must be at least the value returned by
+   compressBound(sourceLen). Upon exit, destLen is the actual size of the
+   compressed buffer.
+
+     compress2 returns Z_OK if success, Z_MEM_ERROR if there was not enough
+   memory, Z_BUF_ERROR if there was not enough room in the output buffer,
+   Z_STREAM_ERROR if the level parameter is invalid.
+*/
+
+ZEXTERN uLong ZEXPORT compressBound OF((uLong sourceLen));
+/*
+     compressBound() returns an upper bound on the compressed size after
+   compress() or compress2() on sourceLen bytes.  It would be used before
+   a compress() or compress2() call to allocate the destination buffer.
+*/
+
+ZEXTERN int ZEXPORT uncompress OF((Bytef *dest,   uLongf *destLen,
+                                   const Bytef *source, uLong sourceLen));
+/*
+     Decompresses the source buffer into the destination buffer.  sourceLen is
+   the byte length of the source buffer. Upon entry, destLen is the total
+   size of the destination buffer, which must be large enough to hold the
+   entire uncompressed data. (The size of the uncompressed data must have
+   been saved previously by the compressor and transmitted to the decompressor
+   by some mechanism outside the scope of this compression library.)
+   Upon exit, destLen is the actual size of the compressed buffer.
+     This function can be used to decompress a whole file at once if the
+   input file is mmap'ed.
+
+     uncompress returns Z_OK if success, Z_MEM_ERROR if there was not
+   enough memory, Z_BUF_ERROR if there was not enough room in the output
+   buffer, or Z_DATA_ERROR if the input data was corrupted or incomplete.
+*/
+
+
+typedef voidp gzFile;
+
+ZEXTERN gzFile ZEXPORT gzopen  OF((const char *path, const char *mode));
+/*
+     Opens a gzip (.gz) file for reading or writing. The mode parameter
+   is as in fopen ("rb" or "wb") but can also include a compression level
+   ("wb9") or a strategy: 'f' for filtered data as in "wb6f", 'h' for
+   Huffman only compression as in "wb1h", or 'R' for run-length encoding
+   as in "wb1R". (See the description of deflateInit2 for more information
+   about the strategy parameter.)
+
+     gzopen can be used to read a file which is not in gzip format; in this
+   case gzread will directly read from the file without decompression.
+
+     gzopen returns NULL if the file could not be opened or if there was
+   insufficient memory to allocate the (de)compression state; errno
+   can be checked to distinguish the two cases (if errno is zero, the
+   zlib error is Z_MEM_ERROR).  */
+
+ZEXTERN gzFile ZEXPORT gzdopen  OF((int fd, const char *mode));
+/*
+     gzdopen() associates a gzFile with the file descriptor fd.  File
+   descriptors are obtained from calls like open, dup, creat, pipe or
+   fileno (in the file has been previously opened with fopen).
+   The mode parameter is as in gzopen.
+     The next call of gzclose on the returned gzFile will also close the
+   file descriptor fd, just like fclose(fdopen(fd), mode) closes the file
+   descriptor fd. If you want to keep fd open, use gzdopen(dup(fd), mode).
+     gzdopen returns NULL if there was insufficient memory to allocate
+   the (de)compression state.
+*/
+
+ZEXTERN int ZEXPORT gzsetparams OF((gzFile file, int level, int strategy));
+/*
+     Dynamically update the compression level or strategy. See the description
+   of deflateInit2 for the meaning of these parameters.
+     gzsetparams returns Z_OK if success, or Z_STREAM_ERROR if the file was not
+   opened for writing.
+*/
+
+ZEXTERN int ZEXPORT    gzread  OF((gzFile file, voidp buf, unsigned len));
+/*
+     Reads the given number of uncompressed bytes from the compressed file.
+   If the input file was not in gzip format, gzread copies the given number
+   of bytes into the buffer.
+     gzread returns the number of uncompressed bytes actually read (0 for
+   end of file, -1 for error). */
+
+ZEXTERN int ZEXPORT    gzwrite OF((gzFile file,
+                                   voidpc buf, unsigned len));
+/*
+     Writes the given number of uncompressed bytes into the compressed file.
+   gzwrite returns the number of uncompressed bytes actually written
+   (0 in case of error).
+*/
+
+ZEXTERN int ZEXPORTVA   gzprintf OF((gzFile file, const char *format, ...));
+/*
+     Converts, formats, and writes the args to the compressed file under
+   control of the format string, as in fprintf. gzprintf returns the number of
+   uncompressed bytes actually written (0 in case of error).  The number of
+   uncompressed bytes written is limited to 4095. The caller should assure that
+   this limit is not exceeded. If it is exceeded, then gzprintf() will return
+   return an error (0) with nothing written. In this case, there may also be a
+   buffer overflow with unpredictable consequences, which is possible only if
+   zlib was compiled with the insecure functions sprintf() or vsprintf()
+   because the secure snprintf() or vsnprintf() functions were not available.
+*/
+
+ZEXTERN int ZEXPORT gzputs OF((gzFile file, const char *s));
+/*
+      Writes the given null-terminated string to the compressed file, excluding
+   the terminating null character.
+      gzputs returns the number of characters written, or -1 in case of error.
+*/
+
+ZEXTERN char * ZEXPORT gzgets OF((gzFile file, char *buf, int len));
+/*
+      Reads bytes from the compressed file until len-1 characters are read, or
+   a newline character is read and transferred to buf, or an end-of-file
+   condition is encountered.  The string is then terminated with a null
+   character.
+      gzgets returns buf, or Z_NULL in case of error.
+*/
+
+ZEXTERN int ZEXPORT    gzputc OF((gzFile file, int c));
+/*
+      Writes c, converted to an unsigned char, into the compressed file.
+   gzputc returns the value that was written, or -1 in case of error.
+*/
+
+ZEXTERN int ZEXPORT    gzgetc OF((gzFile file));
+/*
+      Reads one byte from the compressed file. gzgetc returns this byte
+   or -1 in case of end of file or error.
+*/
+
+ZEXTERN int ZEXPORT    gzungetc OF((int c, gzFile file));
+/*
+      Push one character back onto the stream to be read again later.
+   Only one character of push-back is allowed.  gzungetc() returns the
+   character pushed, or -1 on failure.  gzungetc() will fail if a
+   character has been pushed but not read yet, or if c is -1. The pushed
+   character will be discarded if the stream is repositioned with gzseek()
+   or gzrewind().
+*/
+
+ZEXTERN int ZEXPORT    gzflush OF((gzFile file, int flush));
+/*
+     Flushes all pending output into the compressed file. The parameter
+   flush is as in the deflate() function. The return value is the zlib
+   error number (see function gzerror below). gzflush returns Z_OK if
+   the flush parameter is Z_FINISH and all output could be flushed.
+     gzflush should be called only when strictly necessary because it can
+   degrade compression.
+*/
+
+ZEXTERN z_off_t ZEXPORT    gzseek OF((gzFile file,
+                                      z_off_t offset, int whence));
+/*
+      Sets the starting position for the next gzread or gzwrite on the
+   given compressed file. The offset represents a number of bytes in the
+   uncompressed data stream. The whence parameter is defined as in lseek(2);
+   the value SEEK_END is not supported.
+     If the file is opened for reading, this function is emulated but can be
+   extremely slow. If the file is opened for writing, only forward seeks are
+   supported; gzseek then compresses a sequence of zeroes up to the new
+   starting position.
+
+      gzseek returns the resulting offset location as measured in bytes from
+   the beginning of the uncompressed stream, or -1 in case of error, in
+   particular if the file is opened for writing and the new starting position
+   would be before the current position.
+*/
+
+ZEXTERN int ZEXPORT    gzrewind OF((gzFile file));
+/*
+     Rewinds the given file. This function is supported only for reading.
+
+   gzrewind(file) is equivalent to (int)gzseek(file, 0L, SEEK_SET)
+*/
+
+ZEXTERN z_off_t ZEXPORT    gztell OF((gzFile file));
+/*
+     Returns the starting position for the next gzread or gzwrite on the
+   given compressed file. This position represents a number of bytes in the
+   uncompressed data stream.
+
+   gztell(file) is equivalent to gzseek(file, 0L, SEEK_CUR)
+*/
+
+ZEXTERN int ZEXPORT gzeof OF((gzFile file));
+/*
+     Returns 1 when EOF has previously been detected reading the given
+   input stream, otherwise zero.
+*/
+
+ZEXTERN int ZEXPORT gzdirect OF((gzFile file));
+/*
+     Returns 1 if file is being read directly without decompression, otherwise
+   zero.
+*/
+
+ZEXTERN int ZEXPORT    gzclose OF((gzFile file));
+/*
+     Flushes all pending output if necessary, closes the compressed file
+   and deallocates all the (de)compression state. The return value is the zlib
+   error number (see function gzerror below).
+*/
+
+ZEXTERN const char * ZEXPORT gzerror OF((gzFile file, int *errnum));
+/*
+     Returns the error message for the last error which occurred on the
+   given compressed file. errnum is set to zlib error number. If an
+   error occurred in the file system and not in the compression library,
+   errnum is set to Z_ERRNO and the application may consult errno
+   to get the exact error code.
+*/
+
+ZEXTERN void ZEXPORT gzclearerr OF((gzFile file));
+/*
+     Clears the error and end-of-file flags for file. This is analogous to the
+   clearerr() function in stdio. This is useful for continuing to read a gzip
+   file that is being written concurrently.
+*/
+
+                        /* checksum functions */
+
+/*
+     These functions are not related to compression but are exported
+   anyway because they might be useful in applications using the
+   compression library.
+*/
+
+ZEXTERN uLong ZEXPORT adler32 OF((uLong adler, const Bytef *buf, uInt len));
+/*
+     Update a running Adler-32 checksum with the bytes buf[0..len-1] and
+   return the updated checksum. If buf is NULL, this function returns
+   the required initial value for the checksum.
+   An Adler-32 checksum is almost as reliable as a CRC32 but can be computed
+   much faster. Usage example:
+
+     uLong adler = adler32(0L, Z_NULL, 0);
+
+     while (read_buffer(buffer, length) != EOF) {
+       adler = adler32(adler, buffer, length);
+     }
+     if (adler != original_adler) error();
+*/
+
+ZEXTERN uLong ZEXPORT adler32_combine OF((uLong adler1, uLong adler2,
+                                          z_off_t len2));
+/*
+     Combine two Adler-32 checksums into one.  For two sequences of bytes, seq1
+   and seq2 with lengths len1 and len2, Adler-32 checksums were calculated for
+   each, adler1 and adler2.  adler32_combine() returns the Adler-32 checksum of
+   seq1 and seq2 concatenated, requiring only adler1, adler2, and len2.
+*/
+
+ZEXTERN uLong ZEXPORT crc32   OF((uLong crc, const Bytef *buf, uInt len));
+/*
+     Update a running CRC-32 with the bytes buf[0..len-1] and return the
+   updated CRC-32. If buf is NULL, this function returns the required initial
+   value for the for the crc. Pre- and post-conditioning (one's complement) is
+   performed within this function so it shouldn't be done by the application.
+   Usage example:
+
+     uLong crc = crc32(0L, Z_NULL, 0);
+
+     while (read_buffer(buffer, length) != EOF) {
+       crc = crc32(crc, buffer, length);
+     }
+     if (crc != original_crc) error();
+*/
+
+ZEXTERN uLong ZEXPORT crc32_combine OF((uLong crc1, uLong crc2, z_off_t len2));
+
+/*
+     Combine two CRC-32 check values into one.  For two sequences of bytes,
+   seq1 and seq2 with lengths len1 and len2, CRC-32 check values were
+   calculated for each, crc1 and crc2.  crc32_combine() returns the CRC-32
+   check value of seq1 and seq2 concatenated, requiring only crc1, crc2, and
+   len2.
+*/
+
+
+                        /* various hacks, don't look :) */
+
+/* deflateInit and inflateInit are macros to allow checking the zlib version
+ * and the compiler's view of z_stream:
+ */
+ZEXTERN int ZEXPORT deflateInit_ OF((z_streamp strm, int level,
+                                     const char *version, int stream_size));
+ZEXTERN int ZEXPORT inflateInit_ OF((z_streamp strm,
+                                     const char *version, int stream_size));
+ZEXTERN int ZEXPORT deflateInit2_ OF((z_streamp strm, int  level, int  method,
+                                      int windowBits, int memLevel,
+                                      int strategy, const char *version,
+                                      int stream_size));
+ZEXTERN int ZEXPORT inflateInit2_ OF((z_streamp strm, int  windowBits,
+                                      const char *version, int stream_size));
+ZEXTERN int ZEXPORT inflateBackInit_ OF((z_streamp strm, int windowBits,
+                                         unsigned char FAR *window,
+                                         const char *version,
+                                         int stream_size));
+#define deflateInit(strm, level) \
+        deflateInit_((strm), (level),       ZLIB_VERSION, sizeof(z_stream))
+#define inflateInit(strm) \
+        inflateInit_((strm),                ZLIB_VERSION, sizeof(z_stream))
+#define deflateInit2(strm, level, method, windowBits, memLevel, strategy) \
+        deflateInit2_((strm),(level),(method),(windowBits),(memLevel),\
+                      (strategy),           ZLIB_VERSION, sizeof(z_stream))
+#define inflateInit2(strm, windowBits) \
+        inflateInit2_((strm), (windowBits), ZLIB_VERSION, sizeof(z_stream))
+#define inflateBackInit(strm, windowBits, window) \
+        inflateBackInit_((strm), (windowBits), (window), \
+        ZLIB_VERSION, sizeof(z_stream))
+
+
+#if !defined(ZUTIL_H) && !defined(NO_DUMMY_DECL)
+    struct internal_state {int dummy;}; /* hack for buggy compilers */
+#endif
+
+ZEXTERN const char   * ZEXPORT zError           OF((int));
+ZEXTERN int            ZEXPORT inflateSyncPoint OF((z_streamp z));
+ZEXTERN const uLongf * ZEXPORT get_crc_table    OF((void));
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* ZLIB_H */
diff --git a/ctrl/file.cnf b/ctrl/file.cnf
index a829db1df6deec0997627d8fca1e5f46910de550..f8c9922e30d9c9f20ae71bc5e0484563c395598e 100644
Binary files a/ctrl/file.cnf and b/ctrl/file.cnf differ
diff --git a/ctrl/text.dat b/ctrl/text.dat
index 63906618a2bc63c7d278cee18d4cb2ec8a1b9eed..4424ebd097a9b37cc9f35a5f32248a09ed52cca0 100644
--- a/ctrl/text.dat
+++ b/ctrl/text.dat
@@ -96,7 +96,7 @@
 	" %4u\b\b\b\b\1h%s \1n\1c(\1h\1`?\1n\1c=Menu) (\1h%u\1n\1c of \1h%u\1n\1c): \1n\1~"
 "\r\nYou didn't post message #%d\r\n"                   072 YouDidntPostMsgN
 "\1?Delete message #%u '%s'"                          073 DeletePostQ
-"\1n\1b[\1h\1wI\1n\1b] \1hAutoLogon via IP address      "\     074 UserDefaultsAutoLogon
+"\1n\1b[\1h\1wI\1n\1b] \1hAutoLogon via IP address     "\     074 UserDefaultsAutoLogon
 	"\1n\1b: \1c%s\r\n"	 
 "\1n\r\n\1m%s sent to \1h%s #%u\r\n"                    075 MsgSentToUser
 "\1_\r\n\1y\1hText to search for: "                      076 SearchStringPrompt
@@ -201,8 +201,8 @@
 "Delete Guru file"                                      163 DeleteGuruLogQ
 "\1n\1g\7Telegram from \1n\1h%s\1n\1g on %s:\r\n\1h"    164 TelegramFmt
 "\r\n\r\nYou can't download.\r\n"                       165 R_Download
-"\r\n\1w\1hSearching all directories @ELLIPSIS@\r\n"      166 SearchingAllDirs
-"\1w\1hSearching all libraries @ELLIPSIS@\r\n"            167 SearchingAllLibs
+"\r\n\1w\1hSearching current library @ELLIPSIS@\r\n\1q"   166 SearchingAllDirs
+"\1w\1hSearching all libraries @ELLIPSIS@\r\n\1q"         167 SearchingAllLibs
 "\r\n\1w\1h%u Files Listed.\r\n"                          168 NFilesListed
 "\r\n\1w\1hEmpty directory.\r\n"                          169 EmptyDir
 "\r\n\1n\1cSearching for files "\                         170 NScanHdr
@@ -278,7 +278,7 @@
 "\r\n\7\1r\1h\1iBatch upload queue is full.\1n\r\n"         229 BatchUlQueueIsFull
 "\r\n\1n\1m\1h%s \1n\1madded to batch upload queue"\         230 FileAddedToUlQueue
 	"\1c - Files: \1h%u \1n\1c(\1h%u\1n\1c Max)\r\n"
-"\7\1_\1w\1hNode %2d: \1g%s\1n\1g sent you a file.\r\n"       231 UserToUserXferNodeMsg
+"Unused 231"                                               231 UserToUserXferNodeMsg
 "\1n\1?\1g\1h%s\1y: \1w~B\1yatch download, "\              232 FileInfoPrompt
 	"\1w~E\1yxtended info, "\
 	"\1w~V\1yiew file, "\
@@ -312,14 +312,14 @@
 "\1_\1y\1hCredit value     : \1n"                           257 EditCreditValue
 "\1_\1y\1hTimes downloaded : \1n"                           258 EditTimesDownloaded
 "\1_\1y\1hOpen count       : \1n"                           259 EditOpenCount
-"\1_\1y\1hAlternate Path   : \1n"                           260 EditAltPath
+"UNUSED260"                                                 260 Unused260
 "\r\n\1w\1hYou only have %s credits.\r\n"                 261 YouOnlyHaveNCredits
 "\r\nYou don't have enough credits.\r\n"                262 NotEnoughCredits
 "\r\n\1w\1hNot enough time left to transfer.\r\n"         263 NotEnoughTimeToDl
 "\r\nProtocol, ~Batch, ~Quit, or [~Next]: "             264 ProtocolBatchQuitOrNext
 "\r\nBulk Upload %s %s Directory\r\n"\                  265 BulkUpload
 	"(Enter '-' for description to skip file):\r\n"
-"\1_\1y\1h%s\1w%7uk\1b:"                                     266 BulkUploadDescPrompt
+"\1_\1y\1h%-12s\1w%7uk\1b:"                             266 BulkUploadDescPrompt
 "\r\n\1r\1h\1iNo files in batch queue.\1n"\                 267 NoFilesInBatchQueue
 	"\r\n\r\n\1mUse \1hD\1n\1m or \1hU\1n\1m to add files to the queue.\r\n"
 "\1_\r\n\1y\1hBatch: \1n"                                   268 BatchMenuPrompt
@@ -356,16 +356,14 @@
 "\r\nUploader: %s\r\nFilename: %s\r\n"                  294 TempFileInfo
 "\r\n%s bytes in %u files\r\n"                          295 TempDirTotal
 "\r\n%u files removed.\r\n"                             296 NFilesRemoved
-"\1r\1h\1iAll other nodes should NOT be in use "\          297 ResortWarning
-	"during resort/compression.\1n\r\n"
-"\1-\1c%-15.15s \1y\1h%-25.25s "                            298 ResortLineFmt
-"\1bEmpty\1n\r\n"                                         299 ResortEmptyDir
-"\1wSorting @ELLIPSIS@"                                   300 Sorting
-"\b\b\b\b\b\b\b\b\b\b\1bSorted    \1n\r\n"                301 Sorted
-"\b\b\b\b\b\b\b\b\b\b\1bCompressed %u slots "\           302 Compressed
-	"(%s bytes)\1n\r\n"
-"\1w\1h\r\n%s is already in the queue.\r\n"               303 FileAlreadyInQueue
-"\1w\1h\1/File is not online.\r\n"                       304 FileIsNotOnline
+"Tag this file"                                         297 TagFileQ
+"\1h\1yEnter (space-separated) Tags: "                  298 TagFilePrompt
+"UNUSED299"                                             299 Unused299
+"UNUSED300"                                             300 Unused300
+"UNUSED301"                                             301 Unused301
+"UNUSED302"                                             302 Unused302
+"\1w\1h\r\n%s is already in the queue.\r\n"             303 FileAlreadyInQueue
+"\1w\1h\1/File is not online.\r\n"                      304 FileIsNotOnline
 "\1n\r\n\1m\1h%s \1n\1madded to batch download queue -\r\n"\ 305 FileAddedToBatDlQueue
 	"\1cFiles: \1h%u\1n\1c (\1h%u\1n\1c Max)  Credits: \1h%s\1n\1c"\
 	"  Bytes: \1h%s\1n\1c  Time: \1h%s\r\n"
@@ -379,7 +377,7 @@
 "\1_\1h\1w%s was %sdownloaded by %s\r\n"\                  312 DownloadUserMsg
 	"\1n\1gYou were awarded %s credits.\r\n"
 "partially "                                            313 Partially
-"\r\n\1n\1gLibrary          :\1h (%u) %s"                  314 FiLib
+"\1l\1n\1gLibrary          :\1h (%u) %s"                  314 FiLib
 "\r\n\1n\1gDirectory        :\1h (%u) %s"                  315 FiDir
 "\r\n\1n\1gFilename         :\1h %s"                       316 FiFilename
 "\r\n\1n\1gFile size        :\1h %s (%s) bytes"            317 FiFileSize
@@ -391,9 +389,9 @@
 "\r\n\1n\1gLast downloaded  :\1h %s"                       323 FiDateDled
 "\r\n\1n\1gTimes downloaded :\1h %u"                       324 FiTimesDled
 "\r\n\1n\1gTime to download :\1h %s"                       325 FiTransferTime
-"\r\n\1n\1gAlternate Path   :\1h %s"                       326 FiAlternatePath
-"\r\n\1r\1h\1iInvalid Alternate Path Number: %u\1n"         327 InvalidAlternatePathN
-"\1_\1/\1w\1hFile is currently open by %d user%s.\r\n"    328 FileIsOpen
+"\r\n\1n\1gTags             :\1h %s"                       326 FiTags
+"UNUSED327"                                                327 Unused327
+"\r\n\1n\1gFile %-6.6s      :\1h %s"                       328 FiChecksum
 "\7\7\r\n\1h\1rH\1ba\1gp\1yp\1cy \1mB\1wi\1rr\1gt\1bh\1cd\1ma\1yy "\  329 HappyBirthday
 	"\1wt\1ro \1gy\1bo\1cu\r\n\7\7\1mH\1ya\1wp\1rp\1gy "\
 	"\1bB\1ci\1mr\1yt\1wh\1rd\1ga\1by \1ct\1mo \1yy\1wo\1ru\1g.\1b.\1c.\r\n\r\n"
@@ -430,10 +428,8 @@
 	"same time.\1n\r\n"
 "\7\1r\1h\1i%d critical errors have occurred. "\           359 CriticalErrors
 	"Type ;ERR at main menu.\1n\r\n"
-"\1_\1w\1hYou have %d User to User Transfer%s "\           360 UserXferForYou
-	"waiting for you\r\n"
-"\1_\1w\1hYou have sent %d unreceived User to "\           361 UnreceivedUserXfer
-	"User Transfer%s\r\n"
+"Unused360"                                                360 UserXferForYou
+"Unused361"                                                361 UnreceivedUserXfer
 "Read your mail now"                                    362 ReadYourMailNowQ
 "Sorry, the system is closed to new users.\r\n"         363 NoNewUsers
 "New User Password: "                                   364 NewUserPasswordPrompt
@@ -737,7 +733,7 @@
 "\r\n%u credits have been added to your account.\r\n"  592 CreditedAccount
 "\r\nANSI Capture is now %s\r\n"                        593 ANSICaptureIsNow
 "\1n\1m\r\nRetrieving \1h%s\1n\1m..."                        594 RetrievingFile
-"\1n\r\nAlternate upload path now: %s\r\n"               595 AltULPathIsNow
+"UNUSED595"                                             595 Unused595
 "\r\nPrivate"                                           596 PrivatePostQ
 "\r\n\1_\1y\1hPost to: "                                   597 PostTo
 "\r\nPrivate posts require a destination user "\        598 NoToUser
diff --git a/docs/newfilebase.txt b/docs/newfilebase.txt
new file mode 100644
index 0000000000000000000000000000000000000000..bd085fed90c9be660ddcff39ecb762cd025b8727
--- /dev/null
+++ b/docs/newfilebase.txt
@@ -0,0 +1,244 @@
+_New FileBases_
+
+The new FileBase files are stored in the same database location as before
+(e.g. data/dirs/), but the file extensions are different:
+
+^ Purpose                  ^   Old    ^   New    ^
+| Index (e.g. filenames)   | code.ixb | code.sid |
+| Data (e.g. descriptions) | code.dat | code.shd |
+| Extended Descriptions    | code.exb | code.sdt |
+| Metadata                 | code.dab | code.ini |
+| Allocation Tables        | N/A      | code.sda and code.sha |
+
+If these new filebase file extensions look familiar to you, that's because
+we're using the Synchronet Message Base format/library (v3.0 now) for the
+underlying database. This means that the SMB tools you may be familiar with
+(e.g. CHKSMB, FIXSMB, SMBUTIL) also work on the new filebases.
+
+The conversion of the filebases to the new format should occur automatically
+when you run 'jsexec update' which in turn will execute the program
+'upgrade_to_v319' when appropriate (just one time). Once converted, you can
+delete the old files or leave them in place in case you need to revert back to
+Synchronet v3.18 for any reason. The old filebase files won't harm anything if
+left.
+
+The creation of each new filebase will automatically calculate and store the
+hashes of the contents of the actual files available for download. These
+hashes are useful for duplicate file detection and data integrity assurance.
+If you wish to opt-out of the file hashing (which consumes the majority of the
+time during the upgrade process), you can turn off file hashing in the
+per-directory Toggle Options in SCFG->File Areas. You would have to perform
+this opt-out for the directories of choice *before* you run 'jsexec update' /
+'upgrade_to_v319'. You should not normally need to run 'upgrade_to_v319'.
+
+_Long Filenames_
+
+While filenames stored in the filebases used to be limited to MS-DOS
+compatible 8.3 formatted names, longer filenames are now supported on all
+platforms. Additionally, some previously invalid filename characters (e.g.
+spaces) are now allowed and files without extensions (i.e. no '.' in their
+filename are now supported).
+
+Although Synchronet for Windows previously used Win32 API functions for short
+<-> long filename conversions in the Windows builds of Synchronet, resulting
+in the unfortunate Micros~1 shorted filenames stored and sometimes seen, that
+is no longer the case. Except for the %~ command-line specifier, those
+short/long filename conversion functions are no longer in use anywhere within
+Synchronet for Windows - it's native filenames through-out. The filebase
+conversion process (upgrade_to_v319 / 'jsexec update') on Windows will attempt
+to automatically resolve the native/long filenames and store those names and
+only those names in the new filebases.
+
+Note: abbreviated versions of long filenames are displayed in some situations
+to accommodate the limited width of a traditional BBS user terminal. An effort
+is made to always display the full file extension/suffix however
+(e.g. "longfilename.jpeg" may be *displayed* as "longfil.jpeg").
+
+Note: only 64 characters of each filename (always including any extension) are
+indexed for searches and duplicate checking, but the entire filename, up to
+64K characters in length, is stored intact in the filebase.
+
+Filenames with /extensions/ longer than 3 characters, e.g. ".jpeg", ".tar.gz",
+can be added to the filebases, but the configurable compressible, extractable,
+and viewable file types/extensions remain limited to 3 characters in SCFG.
+Similarly, a maximum length of 3 character archive "types" are stored per BBS
+user record (for each user's QWK packet format and temp archive preference).
+
+_Large Files_
+
+Files greater in size than 2GB or 4GB (depending) were previously a problem.
+Though there are still some 32-bit file length limitations (e.g. only files
+smaller than 4GB in size will be hashed), there is better and increasing
+support for larger files in general.
+
+Note: the ZMODEM transfer protocol as designed by Chuck Forsberg only supports
+files up to 4GB in size and in many cases, files greater than or equal to 2GB
+in size will prove difficult or impossible to transfer between some ZMODEM
+implementations. In general, it is recommended to use an alternate transfer
+protocol (e.g. YMODEM[-G], FTP, HTTP) for files >= 2GB in length.
+
+_Large Directories_
+
+Due to the new filebase design, directories with more than 10,000 files are
+now supported (though, not encouraged).
+
+_Descriptions_
+
+The file "summary" or single-line "short description" remains limited to 58
+characters in length for practical purposes. Though longer file summaries
+(up to 64KB) can be stored in the filebase, they are not recommended.
+
+Extended (multi-line) descriptions may now span more than the previous limit
+of 10 lines or 512 total characters. There is no technical limit to the length
+of extended file descriptions, though a limit of 1024 characters imported from
+description files embedded in archives (e.g. FILE_ID.DIZ) is being imposed. If
+you have a need for longer (than 1024 character) extended descriptions
+imported from embedded description files, please provide me with details.
+
+_Batches_
+
+File upload and download batch queues used to be maintained in memory (though
+they were written to disk files to be retained between user logons), they are
+now entirely maintained in disk files (data/user/*.upload and *.dnload in .ini
+file format). This means that custom batch management can now be performed
+easily by modules or non-Terminal Server scripts.
+
+_Hashes_
+
+Files are now hashed, by default, using multiple hashing algorithms (CRC16,
+CRC32, MD5, and SHA1) for duplicate file detection and for reporting to users
+(e.g. to insure data integrity). For a file to be considered a duplicate
+(i.e. and rejected for upload) it must have the same size and hash values as
+another file in a filebase already. Each directory is configurable as to
+whether or not to hash its files or use it for duplicate file detection
+(by name or hash).
+
+_Sorting_
+
+While the old filebases 
+The new filebases are indexed in the order in which the files are imported
+into the database. Sorting of the files for display purposes in the terminal
+and FTP servers is optional and configured by the sysop:
+ Name Ascending (case-insensitive)
+ Name Descending (case-insensitive)
+ Name Ascending (case-sensitive)
+ Name Descending (case-sensitive)
+ Date Ascending
+ Date Descending
+
+As a result, the "RESORT" file transfer operator command has been removed.
+
+_Tags_
+
+Individual files can now be tagged for easy searching/grouping. This feature
+will be utilized/enhanced more in the future.
+
+_JavaScript_
+
+The new "FileBase" class is used (similar to the existing MsgBase class) to
+open and access filebases from JavaScript modules. Using this class, the
+defaults methods of listing and transferring files can be replaced with
+custom modules.
+
+_TickIT_
+
+The new FileBase JS class is now used to import files directly from FidoNet
+-style .TIC files (via tickit.js) so no dependency or invocation of any
+external utilities (e.g. addfiles) is required.
+
+_Utilities_
+
+The native utilities ADDFILES, FILELIST, DELFILES, and DUPEFIND have been
+replaced with similarly named and purposed JavaScript utility scripts to be
+invoked with JSexec:
+- addfiles.js for importing lists of files into filebases
+- postfile.js for importing a single file into a filebase
+- filelist.js for generating file listings from filebases
+- delfiles.js for removing files from filebases
+- dupefind.js for discovering and reporting duplicate files in the filebases
+
+_Performance_
+
+Due to the nature and use of the new filebase API, file listings are much
+faster (e.g. large file listings from the Synchronet FTP server) as well as
+filename/pattern, description text, and duplicate-file searching.
+
+_FTP Server_
+
+Due to the removal of support for rendering FTP-downloaded content (e.g.
+HTML files) in modern web browsers, the FTP Server no longer supports dynamic
+HTML index file generation (e.g. 00index.html). Instead, we will focus on
+better support for filebase browsing and file transfers via HTTP and HTTPS in
+addition to the traditional FTP and FTPS uses. The dynamic generation of
+the ASCII file listings via FTP (e.g. 00index) is still supported by the FTP
+server, though now much faster than before.
+
+_libarchive_
+
+The libarchive library (http://libarchive.org/) has now been integrated into
+Synchronet (and exposed via the new "Archive" JavaScript class) and integrated
+into SBBSecho so that the creation, listing/viewing, and extraction of
+archived files can now be performed "in-process" without the invocation of or
+dependency on external programs (e.g. Info-Zip unzip or PKUNZIP).
+
+Formats fully supported:
+- zip
+- 7zip
+- gzipped-tar
+- bzipped-tar
+
+Formats supported for viewing and extraction only:
+- rar
+- lha/lzh
+- iso
+- xar
+- cab
+
+This means that for most BBSes, no "Compressible" or "Extractable" file types
+need to be configured in SCFG->File Options. Additionally, by setting
+"Archive Format" to "ZIP" for SCFG->Networks->QWK->Hubs, no "pack" or "unpack"
+command-line need be configured.
+
+For listing the contents of archives, the new archive.js utility script may be
+installed as a "Viewable File Type" handler for the commonly supported file
+extensions by running 'jsexec archive.js install'.
+
+_DIZ_
+
+Description files embedded in archives (e.g. FILE_ID.DIZ) are now supported
+more uniformly and seamlessly.
+
+_File Echoes_
+
+Each file transfer directory configured in SCFG->File Areas may now have an
+"Area Tag" explicitly set for FidoNet-style file distribution networks. If
+an Area Tag is not explicitly set, then the directory's short name is used
+(with spaces replaced with underscores) automatically. tickit.js now uses
+this new "area_tag" file_area.dir[] JS property for its "AutoAreas" feature.
+
+_User to User Files_
+
+The user to user file transfer feature has been removed. Send file attachments
+with email/netmail if you want to send files to users.
+
+_Opened Files_
+
+The "open" (reference) counter for files is now gone. If you want to remove
+a file from the filebase while a user has it in their batch download queue or
+is actively downloading it, nothing is preventing you from doing so.
+
+As a result, the "CLOSE" file transfer operator command has been removed.
+
+_Alternate File Paths_
+
+The support for "Alternate File Paths" has been removed. There are better
+modern operating/file system solutions to the original problem solved with
+this feature.
+
+As a result, the "ALTUL" file transfer operator command has been removed.
+
+_Bi-directional File Transfers_
+
+The protocol drivers that supported bi-directional file transfers (Bi-Modem,
+HS/Link) are now long unsupported DOS/OS2 programs with no equivalent in the
+modern world. Bye bye Bi-modem. :-(
\ No newline at end of file
diff --git a/exec/addfiles.js b/exec/addfiles.js
new file mode 100755
index 0000000000000000000000000000000000000000..35899a27acb4a83b69e47007c99343e537885428
--- /dev/null
+++ b/exec/addfiles.js
@@ -0,0 +1,285 @@
+// Add files to a file base/area directory for SBBS v3.19+
+// Replaces functionality of the old ADDFILES program written in C
+
+require("sbbsdefs.js", 'LEN_FDESC');
+
+"use strict";
+
+const default_excludes = [
+	"FILES.BBS",
+	"FILE_ID.DIZ",
+	"DESCRIPT.ION",
+	"SFFILES.BBS"
+];
+
+if(argv.indexOf("-help") >= 0 || argv.indexOf("-?") >= 0) {
+	print("usage: [-options] [dir-code] [listfile]");
+	print("options:");
+	print("-all            add files in all libraries/directories (implies -auto)");
+	print("-lib=<name>     add files in all directories of specified library (implies -auto)");
+	print("-from=<name>    specify uploader's user name (may require quotes)");
+	print("-ex=<filename>  add to excluded filename list");
+	print("                (default: " + default_excludes.join(',') + ")");
+	print("-diz            always extract/use description in archive");
+	print("-update         update existing file entries (default is to skip them)");
+	print("-date[=fmt]     include today's date in description");
+	print("-fdate[=fmt]    include file's date in description");
+	print("-adate[=fmt]    include newest archived file date in description");
+	print("                (fmt = optional strftime date/time format string)");
+	print("-v              increase verbosity of output");
+	print("-debug          enable debug output");
+	exit(0);
+}
+
+function datestr(t)
+{
+	if(date_fmt)
+		return strftime(date_fmt, t);
+	return system.datestr(t);
+}
+
+function archive_date(file)
+{
+	try {
+		var list = Archive(file).list();
+	} catch(e) {
+		return file_date(file);
+	}
+	var t = 0;
+	for(var i = 0; i < list.length; i++)
+		t = Math.max(list[i].time, t);
+	return t;
+}
+
+var uploader;
+var listfile;
+var date_fmt;
+var options = {};
+var exclude = [];
+var dir_list = [];
+var verbosity = 0;
+for(var i = 0; i < argc; i++) {
+	var arg = argv[i];
+	if(arg[0] == '-') {
+		if(arg.indexOf("-ex=") == 0) {
+			exclude.push(arg.slice(4).toUpperCase());
+			continue;
+		}
+		if(arg.indexOf("-lib=") == 0) {
+			var lib = arg.slice(5);
+			if(!file_area.lib[lib]) {
+				alert("Library not found: " + lib);
+				exit(1);
+			}
+			for(var j = 0; j < file_area.lib[lib].dir_list.length; j++)
+				dir_list.push(file_area.lib[lib].dir_list[j].code);
+			options.auto = true;
+			continue;
+		}
+		if(arg.indexOf("-from=") == 0) {
+			uploader = arg.slice(6);
+			continue;
+		}
+		if(arg.indexOf("-date=") == 0) {
+			date_fmt = arg.slice(6);
+			options.date = true;
+			continue;
+		}
+		if(arg.indexOf("-fdate=") == 0) {
+			date_fmt = arg.slice(7);
+			options.fdate = true;
+			continue;
+		}
+		if(arg.indexOf("-adate=") == 0) {
+			date_fmt = arg.slice(7);
+			options.adate = true;
+			continue;
+		}
+		if(arg == '-' || arg == '-all') {
+			for(var dir in file_area.dir)
+				dir_list.push(dir);
+			options.auto = true;
+			continue;
+		}
+		if(arg[1] == 'v') {
+			var j = 1;
+			while(arg[j++] == 'v')
+				verbosity++;
+			continue;
+		}
+		options[arg.slice(1)] = true;
+	} else {
+		if(!dir_list.length)
+			dir_list.push(arg);
+		else
+			listfile = arg;
+	}
+}
+
+if(exclude.length < 1)
+	exclude = default_excludes;
+if(listfile)
+	exclude.push(listfile.toUpperCase());
+
+if(!dir_list.length) {
+	var code;
+	while(!file_area.dir[code] && !js.terminated) {
+		for(var d in file_area.dir)
+			print(d);
+		code = prompt("Directory code");
+	}
+	dir_list.push(code);
+}
+
+var added = 0;
+var updated = 0;
+for(var d = 0; d < dir_list.length; d++) {
+	
+	var code = dir_list[d];
+	var dir = file_area.dir[code];
+	if(!dir) {
+		alert("Directory '" + code + "' does not exist in configuration");
+		continue;
+	}
+	if(options.auto && (dir.settings & DIR_NOAUTO))
+		continue;
+	print("Adding files to " + dir.lib_name + " " + dir.name);
+	
+	var filebase = new FileBase(code);
+	if(!filebase.open("r")) {
+		alert("Failed to open: " + filebase.file);
+		continue;
+	}
+
+	var name_list = filebase.get_names();
+	// Convert to uppercase
+	for(var i = 0; i < name_list.length; i++) {
+		name_list[i] = name_list[i].toUpperCase();
+		if(options.debug)
+			print(name_list[i]);
+	}
+	var file_list = [];
+
+	if(listfile) {
+		var listpath = file_getcase(dir.path + listfile) || file_getcase(listfile);
+		var f = new File(listpath);
+		if(f.exists) {
+			print("Opening " + f.name);
+			if(!f.open('r')) {
+				alert("Error " + f.error + " (" + strerror(f.error) + ") opening " + f.name);
+				exit(1);
+			}
+			file_list = parse_file_list(f.readAll());
+			f.close();
+		} else {
+			alert(dir.path + file_getname(listfile) + " does not exist");
+		}
+	}
+	else {
+		var list = directory(dir.path + '*');
+		for(var i = 0; i < list.length; i++) {
+			if(!file_isdir(list[i]))
+				file_list.push({ name: file_getname(list[i]) });
+		}
+	}
+	
+	for(var i = 0; i < file_list.length; i++) {
+		var file = file_list[i];
+		file.from = uploader;
+		if(options.debug)
+			print(JSON.stringify(file, null, 4));
+		else if(verbosity)
+			printf("%s ", file.name);
+		if(exclude.indexOf(file.name.toUpperCase()) >= 0) {
+			if(verbosity)
+				print("excluded (ignored)");
+			continue;
+		}
+		file.extdesc = lfexpand(file.extdesc);
+		if(verbosity > 1)
+			print(JSON.stringify(file));
+		var exists = name_list.indexOf(filebase.get_name(file.name).toUpperCase()) >= 0;
+		if(exists && !options.update) {
+			if(verbosity)
+				print("already added");
+			continue;
+		}
+		var path = file_area.dir[code].path + file.name;
+		if(!file_exists(path)) {
+			alert("does not exist: " + path);
+			continue;
+		}
+		if(options.date)
+			file.desc = datestr(time()) + " " + file.desc;
+		else if(options.fdate)
+			file.desc = datestr(file_date(path)) + " " + file.desc;
+		else if(options.adate)
+			file.desc = datestr(archive_date(path)) + " " + file.desc;
+		file.cost = file_size(path);
+		if(exists) {
+			var hash = filebase.hash(file.name);
+			if(hash) {
+				file.size = hash.size;
+				file.crc16 = hash.crc16;
+				file.crc32 = hash.crc32;
+				file.md5 = hash.md5;
+				file.sha1 = hash.sha1;
+			}
+			if(!filebase.update(file.name, file, options.diz)) {
+				alert("Error " + filebase.last_error + " updating " + file.name);
+			} else {
+				print("Updated " + file.name);
+				updated++;
+			}
+		} else {
+			// Add file here:
+			if(!filebase.add(file, options.diz)) {
+				alert("Error " + filebase.last_error + " adding " + file.name);
+			} else {
+				print("Added " + file.name);
+				added++;
+			}
+		}
+	}
+	
+	filebase.close();
+}
+print(added + " files added");
+if(updated)
+	print(updated + " files updated");
+
+// Parse a FILES.BBS (or similar) file listing file
+// Note: file descriptions must begin with an alphabetic character
+function parse_file_list(lines)
+{
+	var file_list = [];
+	for(var i = 0; i < lines.length; i++) {
+		var line = lines[i];
+		var match = line.match(/(^[\w]+[\w\-\!\#\.]*)\W+[^A-Za-z]*(.*)/);
+//		print('fname line match: ' + JSON.stringify(match));
+		if(match && match.length > 1) {
+			var file = { name: match[1], desc: match[2] };
+			if(file.desc && file.desc.length > LEN_FDESC)
+				file.extdesc = word_wrap(file.desc, 45);
+			file_list.push(file);
+			continue;
+		}
+		match = line.match(/\W+\|\s+(.*)/);
+		if(!match) {
+			if(verbosity)
+				alert("Ignoring line: " + line);
+			continue;
+		}
+//		print('match: ' + JSON.stringify(match));
+		if(match && match.length > 1 && file_list.length) {
+			var file = file_list[file_list.length - 1];
+			if(!file.extdesc)
+				file.extdesc = file.desc + "\n";
+			file.extdesc += match[1] + "\n";
+			var combined = file.desc + " " + match[1].trim();
+			if(combined.length <= LEN_FDESC)
+				file.desc = combined;
+		}
+	}
+	return file_list;
+}
diff --git a/exec/archive.js b/exec/archive.js
new file mode 100755
index 0000000000000000000000000000000000000000..f924821b2dfe482b3b2b0a5702e84634ac2afa2b
--- /dev/null
+++ b/exec/archive.js
@@ -0,0 +1,121 @@
+// Deal with archive files using Synchronet v3.19 Archive class
+
+// Install "Viewable File Types" using 'jsexec archive.js install'
+
+"use strict";
+
+var cmd = argv.shift();
+var fname = argv.shift();
+var verbose = false;
+var i = argv.indexOf('-v');
+if(i >= 0) {
+	verbose = true;
+	argv.splice(i, 1);
+}
+switch(cmd) {
+	case 'list':
+		list(fname, verbose);
+		break;
+	case 'json':
+		writeln(JSON.stringify(Archive(fname).list(verbose, argv[0]), null, 4));
+		break;
+	case 'create':
+		print(Archive(fname).create(directory(argv[0])) + " files archived");
+		break;
+	case 'extract':
+		var a = Archive(fname);
+		print(a.extract.apply(a, argv) + " files extracted");
+		break;
+	case 'read':
+		print(Archive(fname).read(argv[0]));
+		break;
+	case 'type':
+		print(Archive(fname).type);
+		break;
+	case 'install':
+		install();
+		break;
+	default:
+		throw new Error("invalid command: " + cmd);
+}
+
+function list(filename, verbose)
+{
+	var list;
+	try {
+		 list = Archive(filename).list(verbose);
+	} catch(e) {
+		alert(file_getname(filename) + ": Unsupported archive format");
+		return;
+	}
+	
+	var dir_fmt = "\x01n%s";
+	var file_fmt = "\x01n \x01c\x01h%-*s \x01n\x01c%10lu  ";
+	if(verbose)
+		file_fmt += "\x01h%08lX  ";
+	file_fmt += "\x01h\x01w%s";
+	if(!js.global.console) {
+		dir_fmt = strip_ctrl(dir_fmt);
+		file_fmt = strip_ctrl(file_fmt);
+	}
+	var longest_name = 0;
+	for(var i = 0; i < list.length; i++) {
+		longest_name = Math.max(longest_name, file_getname(list[i].name).length);
+	}
+	var curpath;
+	for(var i = 0; i < list.length && !js.terminated && (!js.global.console || !console.aborted); i++) {
+		if(list[i].type != "file")
+			continue;
+		else {
+			var fname = file_getname(list[i].name);
+			var path = list[i].name.slice(0, -fname.length);
+			if(path != curpath)
+				writeln(format(dir_fmt, path ? path : "[root]"));
+			if(verbose)
+				writeln(format(file_fmt
+					,longest_name, fname, list[i].size, list[i].crc32
+					,system.timestr(list[i].time).slice(4)));
+			else
+				writeln(format(file_fmt
+					,longest_name, fname, list[i].size
+					,system.timestr(list[i].time).slice(4)));
+			curpath = path;
+		}
+	}
+}
+
+function install()
+{
+	var viewable_exts = [
+		'7z',
+		'exe',
+		'bz',
+		'gz',
+		'iso',
+		'lha',
+		'lzh',
+		'tbz',
+		'tgz',
+		'rar',
+		'xar',
+		'zip'
+	];
+	
+	var cnflib = load({}, "cnflib.js");
+	var file_cnf = cnflib.read("file.cnf");
+	if(!file_cnf) {
+		alert("Failed to read file.cnf");
+		exit(-1);
+	}
+	for(var e in viewable_exts) {
+		file_cnf.fview.push({
+			extension: viewable_exts[e],
+			cmd: '?archive list %f'
+			});
+	}
+	if(!cnflib.write("file.cnf", undefined, file_cnf)) {
+		alert("Failed to write file.cnf");
+		exit(-1);
+	}
+	exit(0);
+}	
diff --git a/exec/default.src b/exec/default.src
index c8774ea630f9466250b4756961cfc62606206da6..455c9f08b9faccdab978939e20cc6cbdaa1d1540 100644
--- a/exec/default.src
+++ b/exec/default.src
@@ -746,7 +746,7 @@ cmdkey J
 	end_cmd
 
 cmdkey L
-	setstr *.*
+	setstr *
 	file_list
 	end_cmd
 
diff --git a/exec/filelist.js b/exec/filelist.js
new file mode 100755
index 0000000000000000000000000000000000000000..a1b17607b581f11fa631da1d9440598e65f62725
--- /dev/null
+++ b/exec/filelist.js
@@ -0,0 +1,142 @@
+// List files in a Synchronet v3.19 file base directory
+
+"use strict";
+
+var options = { sort: false};
+var detail = -1;
+var dir_list = [];
+var filespec = "";
+var props = [];
+var fmt;
+for(var i = 0; i < argc; i++) {
+	var arg = argv[i];
+	if(arg[0] == '-') {
+		var opt = arg.slice(1);
+		if(opt[0] == 'v') {
+			var j = 0;
+			while(opt[j++] == 'v')
+				detail++;
+			continue;
+		}
+		if(opt.indexOf("p=") == 0) {
+			props.push(opt.slice(2));
+			continue;
+		}
+		if(opt == "json") {
+			fmt = "json";
+			continue;
+		}
+		if(opt == "arc") {
+			fmt = "arc";
+			continue;
+		}
+		if(opt.indexOf("fmt=") == 0) {
+			fmt = opt.slice(4);
+			continue;
+		}
+		if(opt == "all") {
+			for(var dir in file_area.dir)
+				dir_list.push(dir);
+			continue;
+		}
+		options[opt] = true;
+		continue;
+	}
+	if(file_area.dir[arg])
+		dir_list.push(arg);
+	else
+		filespec = arg;
+}
+if(props.length < 1)
+	props = ["name", "size", "from", "desc", "extdesc"];
+if(!fmt) {
+	fmt = "%-13s %10s  %-25s  %s";
+	if(detail > 1)
+		fmt += "\n%s";
+}
+
+var output = [];
+for(var i in dir_list) {
+	var dir_code = dir_list[i];
+	var dir = file_area.dir[dir_code];
+	if(!dir) {
+		alert("dir not found: " + dir_code);
+		continue;
+	}
+	if(options.hdr) {
+		var hdr = format("%-15s %-40s Files: %d", dir.lib_name, dir.description, dir.files);
+		output.push(hdr);
+		output.push(format("%.*s", hdr.length
+			, "-------------------------------------------------------------------------------"));
+	}
+	output = output.concat(listfiles(dir_code, filespec, detail, fmt, props));
+}
+//if(options.sort)
+//	output.sort();
+for(var i in output)
+	print(output[i]);
+
+function archive_contents(path, list)
+{
+	var output = [];
+	for(var i = 0; i < list.length; i++) {
+		var fname = path + list[i];
+		print(fname);
+		output.push(fname);
+		var contents;
+		try {
+			contents = Archive(fname).list();
+		} catch(e) {
+//			alert(e);
+			continue;
+		}
+		for(var j = 0; j < contents.length; j++)
+			output.push(contents[j].name + " " + contents[j].size);
+	}
+	return output;
+}
+
+function listfiles(dir_code, filespec, detail, fmt, props)
+{
+	var base = new FileBase(dir_code);
+	if(!base.open())
+		return base.last_error;
+	var output = [];
+	if(detail < 0) {
+		var list = base.get_names(filespec, options.sort);
+		if(fmt == 'json')
+			output = JSON.stringify(list, null, 4).split('\n');
+		else if(fmt == 'arc')
+			output = archive_contents(file_area.dir[dir_code].path, list);
+		else
+			output = list;
+	} else {
+		var list = base.get_list(filespec, detail, options.sort);
+		if(fmt == 'json')
+			output.push(JSON.stringify(list, null, 4));
+		else {
+			for(var i = 0; i < list.length; i ++)
+				output.push(list_file(list[i], fmt, props));
+		}
+	}
+	base.close();
+	return output;
+}
+
+function list_file(file, fmt, props)
+{
+	if(typeof file == 'string') {
+		print(file);
+		return;
+	}
+	if(fmt === undefined)
+		fmt = "%s";
+	var a = [fmt];
+	for(var i in props) {
+		if(file[props[i]] === undefined)
+			a.push('');
+		else
+			a.push(file[props[i]]);
+	}
+	return format.apply(this, a);
+}
diff --git a/exec/hashfile.js b/exec/hashfile.js
new file mode 100755
index 0000000000000000000000000000000000000000..02100b6d8f2fd756d0c17f9b9c52fec5db4be49f
--- /dev/null
+++ b/exec/hashfile.js
@@ -0,0 +1,31 @@
+"use strict";
+
+if(argv.indexOf("-help") >= 0 || argv.indexOf("-?") >= 0) {
+	print("usage: [dir-code] [file-name]");
+	exit(0);
+}
+
+var code = argv[0];
+
+while(!file_area.dir[code] && !js.terminated) {
+	for(var d in file_area.dir)
+		print(d);
+	code = prompt("Directory code");
+}
+
+var dir = file_area.dir[code];
+
+var filebase = new FileBase(code);
+if(!filebase.open()) {
+	alert("Failed to open: " + filebase.file);
+	exit(1);
+}
+
+print(JSON.stringify(filebase.hash(argv[1]), null, 4));
+
+print(filebase.last_error);
+/*
+
+var name_list = filebase.get_file_names();
+
+*/
\ No newline at end of file
diff --git a/exec/jsdocs.js b/exec/jsdocs.js
index 8418667fa022853bde8d1260632ffd80affe28ae..9a42d2976d77337f6bee5704c61f0044520c9f0b 100644
--- a/exec/jsdocs.js
+++ b/exec/jsdocs.js
@@ -3,8 +3,6 @@
 // This script generates HTML documentation of the Synchronet JavaScript object model
 // Requires a Debug build of the Synchronet executable(s)
 
-// $Id: jsdocs.js,v 1.40 2020/04/20 06:31:15 rswindell Exp $
-
 const table_tag = "<table border=1 width=100%>";
 
 const li_tag =	"<li onclick = 'this.className = (this.className == \"showList\") ? \"defaultStyles\" : \"showList\";'\n" +
@@ -320,7 +318,9 @@ if(js.global.msg_area != undefined)		document_object("msg_area"	,msg_area);
 if(js.global.file_area != undefined)	document_object("file_area"	,file_area);
 if(js.global.xtrn_area != undefined)	document_object("xtrn_area"	,xtrn_area);
 if(js.global.MsgBase != undefined)		document_object("MsgBase"	,new MsgBase(msg_area.grp_list[0].sub_list[0].code), "class");
+if(js.global.FileBase != undefined)		document_object("FileBase"	,new FileBase(file_area.lib_list[0].dir_list[0].code), "class");
 if(js.global.File != undefined)			document_object("File"		,new File(system.devnull), "class");
+if(js.global.Archive != undefined)		document_object("Archive"	,new Archive(system.devnull), "class");
 if(js.global.Queue != undefined)		document_object("Queue"		,new Queue(), "class");
 if(js.global.Socket != undefined) {
 	var sock=new Socket();
diff --git a/exec/load/avatar_lib.js b/exec/load/avatar_lib.js
index 02d8ee57b4a5e433cc77521be1a41ca0c10f602e..d110dcd497e4a6874f94a3d79c6146bafd204686 100644
--- a/exec/load/avatar_lib.js
+++ b/exec/load/avatar_lib.js
@@ -195,6 +195,8 @@ function read_netuser(username, netaddr)
 function read(usernum, username, netaddr, bbsid)
 {
 	var usernum = parseInt(usernum, 10);
+	if(!usernum && !username)
+		return false;
 	var obj = cache_get(usernum >= 1 ? usernum : username, netaddr);
 	if(obj !== undefined)	// null and false are also valid cached avatar values
 		return obj;
diff --git a/exec/load/fidocfg.js b/exec/load/fidocfg.js
index 48fea276f41eff0d930cb72185f4cf7b4f4fe654..5542abbf3bb298e7602600f6543bbc06e0f3449c 100644
--- a/exec/load/fidocfg.js
+++ b/exec/load/fidocfg.js
@@ -82,9 +82,7 @@ function TickITCfg(fname) {
 		var dir = file_area.dir[code];
 		if(auto_areas.indexOf(dir.lib_name) < 0)
 			continue;
-		if(dir.name.indexOf(' ') >= 0) // Invalid areatag
-			continue;
-		this.acfg[dir.name.toLowerCase()] = { dir: code };
+		this.acfg[dir.area_tag.toLowerCase()] = { dir: code };
 	}
 	sects = tcfg.iniGetSections();
 	for (i=0; i<sects.length; i++) {
diff --git a/exec/load/sbbslist_lib.js b/exec/load/sbbslist_lib.js
index 2be83a4130298e9e2ffada8a5291c53a3c2723ee..390cf8e37bf1704492ff0b5a6391dab20b10d231 100644
--- a/exec/load/sbbslist_lib.js
+++ b/exec/load/sbbslist_lib.js
@@ -13,7 +13,7 @@ var sort_property = 'name';
 
 // These max lengths are derived from the bbs_t structure definition in xtrn/sbl/sbldefs.h:
 const max_len = {
-	name:				25,		/* Synchronet allows 40, I think this restricted by 25-char QWK msg subjs in sbldefs.h */
+	name:				40,		/* Synchronet allows 40, I think this restricted by 25-char QWK msg subjs in sbldefs.h */
 	phone_number:		25,		/* only the first 12 chars are backwards compatible with SBL v3 */
 	location:			30,
 	sysop_name:			25,
@@ -658,11 +658,11 @@ function new_system(name, nodes, stats)
 function check_entry(bbs)
 {
 	if(!bbs.name || !bbs.name.length || bbs.name.length > max_len.name) {
-		log("Problem with BBS name: " + bbs.name);
+		log(LOG_WARNING, "Problem with BBS name: " + bbs.name);
 		return false;
 	}
 	if(!bbs.service || !bbs.service.length) {
-		log(bbs.name + " has no services");
+		log(LOG_WARNING, bbs.name + " has no services");
 		return false;
 	}
 /** this is valid in SBL
@@ -672,7 +672,7 @@ function check_entry(bbs)
 	}
 **/
 	if(!bbs.entry || !bbs.entry.created || !bbs.entry.created.by) {
-		log(bbs.name + " has no entry.created.by property");
+		log(LOG_WARNING, bbs.name + " has no entry.created.by property");
 		return false;
 	}
 	return true;	// All is good
diff --git a/exec/load/text.js b/exec/load/text.js
index c88269c30e68f44953f3656d1fae41257e14c1cd..b923fac29bfc45a7aadd0f36241a1f4cdaf7659d 100644
--- a/exec/load/text.js
+++ b/exec/load/text.js
@@ -266,7 +266,7 @@ var EditUploader=256;
 var EditCreditValue=257;
 var EditTimesDownloaded=258;
 var EditOpenCount=259;
-var EditAltPath=260;
+var Unused260=260;
 var YouOnlyHaveNCredits=261;
 var NotEnoughCredits=262;
 var NotEnoughTimeToDl=263;
@@ -303,12 +303,12 @@ var TempFileNotCreatedYet=293;
 var TempFileInfo=294;
 var TempDirTotal=295;
 var NFilesRemoved=296;
-var ResortWarning=297;
-var ResortLineFmt=298;
-var ResortEmptyDir=299;
-var Sorting=300;
-var Sorted=301;
-var Compressed=302;
+var TagFileQ=297;
+var TagFilePrompt=298;
+var Unused299=299;
+var Unused300=300;
+var Unused301=301;
+var Unused302=302;
 var FileAlreadyInQueue=303;
 var FileIsNotOnline=304;
 var FileAddedToBatDlQueue=305;
@@ -332,9 +332,9 @@ var FiDateUled=322;
 var FiDateDled=323;
 var FiTimesDled=324;
 var FiTransferTime=325;
-var FiAlternatePath=326;
-var InvalidAlternatePathN=327;
-var FileIsOpen=328;
+var FiTags=326;
+var Unused327=327;
+var FiChecksum=328;
 var HappyBirthday=329;
 var TimeToChangePw=330;
 var NewPasswordQ=331;
@@ -601,7 +601,7 @@ var Convert100ktoNminQ=591;
 var CreditedAccount=592;
 var ANSICaptureIsNow=593;
 var RetrievingFile=594;
-var AltULPathIsNow=595;
+var Unused595=595;
 var PrivatePostQ=596;
 var PostTo=597;
 var NoToUser=598;
diff --git a/exec/pcboard.src b/exec/pcboard.src
index d360f3ed6538af15d6d642cb364a9a5c65767d79..5bcc3448c93278e262ca378ac9964d17708c67aa 100644
--- a/exec/pcboard.src
+++ b/exec/pcboard.src
@@ -46,7 +46,7 @@ cmdstr HELP
 cmdstr F
 	file_select_area
 	if_true
-		setstr "*.*"
+		setstr "*"
 		file_list
 		end_if
 	end_cmd
@@ -54,7 +54,7 @@ cmdstr F
 cmdstr EXT
 	file_select_area
 	if_true
-		setstr "*.*"
+		setstr "*"
 		file_list_extended
 		end_if
 	end_cmd
diff --git a/exec/postfile.js b/exec/postfile.js
new file mode 100755
index 0000000000000000000000000000000000000000..fafed631327116fa4dabdea2bc5759acc9a4e41a
--- /dev/null
+++ b/exec/postfile.js
@@ -0,0 +1,57 @@
+"use strict";
+
+if(argv.indexOf("-help") >= 0 || argv.indexOf("-?") >= 0) {
+	print("usage: [dir-code] [file-name] [file-description] [uploader-name]");
+	exit(0);
+}
+
+var code = argv[0];
+var file = { name: argv[1], desc: argv[2], from: argv[3] };
+
+while(!file_area.dir[code] && !js.terminated) {
+	for(var d in file_area.dir)
+		print(d);
+	code = prompt("Directory code");
+}
+
+var dir = file_area.dir[code];
+
+var filebase = new FileBase(code);
+if(!filebase.open()) {
+	alert("Failed to open: " + filebase.file);
+	exit(1);
+}
+
+var name_list = filebase.get_names();
+
+while(!file_exists(dir.path + file.name) && !js.terminated) {
+	if(file.name)
+		alert(dir.path + file.name + " does not exist");
+	var list = directory(dir.path + '*');
+	for(var i = 0; i < list.length; i++) {
+		if(!file_isdir(list[i]) && name_list.indexOf(file_getname(list[i])) < 0)
+			print(file_getname(list[i]));
+	}
+	file.name = prompt("File name");
+}
+
+if(filebase.get(file.name)) {
+	alert("File '" + file.name + "' already added.");
+	exit(1);
+}
+
+while(!file.desc && !js.terminated) {
+	file.desc = prompt("Description");
+}
+
+while(!file.from && !js.terminated) {
+	file.from = prompt("Uploader");
+}
+
+print("Adding " + file.name + " to " + filebase.file);
+if(filebase.add(file))
+	print(format("File (%s) added successfully to: ", file.name) + code);
+else
+	alert("Error " + filebase.last_error + " adding file to: " + code);
+filebase.close();
+
diff --git a/exec/rehashfiles.js b/exec/rehashfiles.js
new file mode 100755
index 0000000000000000000000000000000000000000..19347183f6fbb63626d360b60fe32253c13fa15e
--- /dev/null
+++ b/exec/rehashfiles.js
@@ -0,0 +1,42 @@
+"use strict";
+
+if(argv.indexOf("-help") >= 0 || argv.indexOf("-?") >= 0) {
+	print("usage: [dir-code] [file-name]");
+	exit(0);
+}
+
+var code = argv[0];
+
+while(!file_area.dir[code] && !js.terminated) {
+	for(var d in file_area.dir)
+		print(d);
+	code = prompt("Directory code");
+}
+
+var dir = file_area.dir[code];
+
+var filebase = new FileBase(code);
+if(!filebase.open()) {
+	alert("Failed to open: " + filebase.file);
+	exit(1);
+}
+
+var file_list = filebase.get_list();
+for(var i = 0; i < file_list.length; i++) {
+	var file = file_list[i];
+	print(JSON.stringify(file, null, 4));
+	var hash = filebase.hash(file.name);
+	if(hash == null) {
+		alert("hash is null");
+		break;
+	}
+	file.size = hash.size;
+	file.crc16 = hash.crc16;
+	file.crc32 = hash.crc32;
+	file.md5 = hash.md5;
+	file.sha1 = hash.sha1;
+	if(!filebase.update(file.name, file)) {
+		alert(filebase.status + " " + filebase.last_error);
+		break;
+	}
+}
diff --git a/exec/sbbslist.js b/exec/sbbslist.js
index 8ed1088639b248fc49d0182beaea9f003b12fa2d..7c107c5ea0ba118911b7dd711f5f97bf586fd8ec 100644
--- a/exec/sbbslist.js
+++ b/exec/sbbslist.js
@@ -267,7 +267,7 @@ function import_entry(name, text)
         if(debug) print(match[1] + " = " + match[2]);
         switch(match[1].toLowerCase()) {
             case 'birth':
-		if(match[2] && match[2].length)
+				if(match[2] && match[2].length)
                 	bbs.first_online = date_from_str(match[2]);
                 break;
             case 'software':
@@ -432,17 +432,17 @@ function import_from_msgbase(list, msgbase, import_ptr, limit, all)
             if(!list[l].entry)
                 continue;
             if(!list[l].imported && hdr.from_net_type) {
-                print(msg_from + " attempted to update/over-write local entry: " + bbs_name);
+                alert(msg_from + " attempted to update/over-write local entry: " + bbs_name);
                 continue;
             }
 			entry = list[l].entry;
             if(entry.created.by.toLowerCase() != hdr.from.toLowerCase()
 				|| (entry.created.at && entry.created.at != hdr.from_net_addr)) {
-                print(msg_from  + " did not create entry: " 
+                alert(msg_from  + " did not create entry: "
 					+ bbs_name + " (" + entry.created.by + "@" + entry.created.at + " did)");
                 continue;
             }
-            print((sbl_remove ? "Removing" : "Updating") 
+            print((sbl_remove ? "Removing" : "Updating")
 				+ " existing entry: " + bbs_name + " (by " + entry.created.by + ")");
 			if(sbl_remove) {
 				if(!lib.remove(entry))
@@ -1606,7 +1606,7 @@ function view(list, current)
 		printf("\1n  ");
 		printf(fmt, "Name\1w", lib.max_len.name, lib.max_len.name, bbs.name);
 		if(bbs.first_online)
-			printf("\1n\1c since \1h%s", bbs.first_online.substring(0,10));
+			printf("\1n\1c est \1h%s", bbs.first_online.substring(0,10));
 		if(bbs.software) {
 			console.attributes = LIGHTGRAY;
 			right_justify(bbs.software);
@@ -2697,4 +2697,4 @@ function main()
 	return 0;
 }
 
-exit(main());
\ No newline at end of file
+exit(main());
diff --git a/exec/simple.src b/exec/simple.src
index 00a47bda776a75f653585ea6a314cc9387b1a4f0..442c1363257ec55877a69026e1fd1403d5f2277f 100644
--- a/exec/simple.src
+++ b/exec/simple.src
@@ -219,7 +219,7 @@ cmdstr F
 		if_false
 			end_cmd
 			end_if
-		setstr "*.*"
+		setstr "*"
 		file_list
 		end_cmd
 
diff --git a/exec/tickit.js b/exec/tickit.js
index 5be49868704f4683ab8011a7148e1aa23b75cefb..c3d84258e079d819e331a901d836aec86ca82da1 100644
--- a/exec/tickit.js
+++ b/exec/tickit.js
@@ -1,6 +1,5 @@
 /*
  * An intentionally simple TIC handler for Synchronet.
- * $Id: tickit.js,v 1.56 2020/05/16 20:11:37 rswindell Exp $
  *
  * How to set up... add a timed event:
  * Internal Code                   TICKIT
@@ -23,11 +22,13 @@
  * flag /sbbs/data/tickit.now *.tic *.TIC
  */
 
-load("sbbsdefs.js");
+require("sbbsdefs.js", 'LEN_FDESC');
 require("fidocfg.js", 'TickITCfg');
 require("fido.js", 'FIDO');
 
 var cfgfile;
+var force_replace = false;
+var use_diz_always = true;
 
 for (var i in argv) {
 	if(argv[i] == "-force-replace")
@@ -37,11 +38,13 @@ for (var i in argv) {
 }
 
 var tickit = new TickITCfg(cfgfile);
+if(tickit.gcfg.forcereplace === true)
+	force_replace = true;
 var sbbsecho = new SBBSEchoCfg(tickit.gcfg.echocfg);
-var files_bbs={};
-var force_replace = false;
+var file_list = {};
+var files_imported = 0;
 
-const REVISION = "$Revision: 1.56 $".split(' ')[1];
+const REVISION = "2.0";
 
 var tickitVersion = "TickIT "+REVISION;
 // emit tickitVersion to the log for general purposes - wk42
@@ -49,9 +52,6 @@ log(LOG_INFO, tickitVersion);
 // emit system.temp_dir to the log for debug purposes; mainly with which
 // temp directory is used when tickit.js is executed (event vs jsexec) - wk42
 log(LOG_DEBUG, "Using system.temp_dir = '"+system.temp_dir+"'");
-// also let's log the logs_dir so we know where it is for the addfiles
-// log if addfileslogcap is set in the ini... this is a manual setting - wk42
-log(LOG_DEBUG, "Using system.logs_dir = '"+system.logs_dir+"'");
 
 if (!String.prototype.repeat) {
   String.prototype.repeat = function(count) {
@@ -116,7 +116,6 @@ function process_tic(tic)
 	var handler;
 	var handler_arg;
 
-	log(LOG_INFO, "Processing...");
 	log(LOG_INFO, "Working with '"+tic.file+"' in '"+tic.area.toUpperCase()+"'.");
 
 	if (tickit.gcfg.path !== undefined)
@@ -144,9 +143,9 @@ function process_tic(tic)
 			handler = cfg.handler;
 			handler_arg = cfg.handlerarg;
 		}
-		if (cfg.forcereplace !== undefined) {
+		if (cfg.forcereplace === true) {
 			log(LOG_INFO, "ForceReplace enabled for area "+tic.area.toUpperCase()+".");
-			force_replace_area = cfg.forcereplace;
+			force_replace_area = true;
 		}
 	}
 
@@ -216,16 +215,11 @@ function process_tic(tic)
 	}
 
 	if (dir !== undefined) {
-		if (files_bbs[dir] === undefined)
-			files_bbs[dir] = '';
-
-		files_bbs[dir] += format("%-12s %10s ", tic.file, tic.size);
-		ld = tic.ldesc.split(/\r?\n/);
-		for (i=0; i<ld.length; i++) {
-			if (i)
-				files_bbs[dir] += " ".repeat(24);
-			files_bbs[dir] += ld[i]+"\r\n";
-		}
+		if (file_list[dir] === undefined)
+			file_list[dir] = [];
+		file = { name: tic.file, cost: tic.size, desc: tic.desc, extdesc: tic.ldesc };
+		file_list[dir].push(file);
+//		log(LOG_INFO, JSON.stringify(file));
 	}
 	log(LOG_INFO, "Deleting TIC file '"+tic.tic_filename+"'.");
 	file_remove(tic.tic_filename);
@@ -548,6 +542,11 @@ function parse_ticfile(fname)
 
 			if (key !== 'desc' && key !== 'ldesc')
 				key = key.replace(/^\s*/,'');
+			if (key == 'desc' && tic.desc) {
+				if (!tic.ldesc)
+					tic.ldesc = tic.desc;
+				key = 'ldesc';
+			}
 			switch(key) {
 				// These are not passed unmodified.
 				// Single value, single line...
@@ -564,7 +563,7 @@ function parse_ticfile(fname)
 				case 'path':
 					// log the path lines for informational purposes before we apply
 					// the circular path detection. - wk42
-					log(LOG_INFO, "Path "+val);
+					log(LOG_DEBUG, "Path "+val);
 					// Circular path detection...
 					for (i=0; i<system.fido_addr_list.length; i++) {
 						if (val === system.fido_addr_list[i]) {
@@ -582,7 +581,7 @@ function parse_ticfile(fname)
 					// we'll log this one for informational purposes and throw it
 					// away so we can create our own later in forward_tic() with our
 					// tickit.js revision line. - wk42
-					log(LOG_INFO, "Created "+val);
+					log(LOG_DEBUG, "Created "+val);
 					break;
 
 				// All the rest are passed through unmodified
@@ -597,6 +596,7 @@ function parse_ticfile(fname)
 				case 'magic':
 				case 'replaces':
 				case 'crc':
+				case 'desc':
 					outtic.push(line);
 					tic[key] = val;
 					break;
@@ -607,7 +607,6 @@ function parse_ticfile(fname)
 					break;
 
 				// Multi-line values
-				case 'desc':
 				case 'ldesc':
 					outtic.push(line);
 					if (tic[key] === undefined)
@@ -622,10 +621,11 @@ function parse_ticfile(fname)
 			}
 		}
 	}
-
-	if (tic.ldesc === undefined || tic.ldesc.length <= tic.desc.length)
-		tic.ldesc = tic.desc;
-
+	if (tic.desc !== undefined) {
+		if(tic.desc.length > LEN_FDESC && !tic.ldesc)
+			tic.ldesc = tic.desc.replace("  ", "\r\n");
+		tic.desc = format("%.*s", LEN_FDESC, tic.desc.trim());
+	}
 	f.close();
 	f = new File(dir+tic.file);
 	if (!f.exists) {
@@ -637,6 +637,7 @@ function parse_ticfile(fname)
 		log(LOG_WARNING, "File '"+f.name+"' length mismatch. File is "+f.length+", expected "+tic.size+".");
 		return false;
 	}
+	tic.size = f.length;
 	if (tic.crc !== undefined) {
 		// File needs to be open to calculate the CRC32.
 		if (!f.open("rb")) {
@@ -656,10 +657,10 @@ function parse_ticfile(fname)
 	else {
 		// there may or may not be @domain on the from line in the
 		// TIC file. look for both address forms. - wk42
-		log(LOG_INFO, "Verifying password for sender: "+tic.from);
+		log(LOG_DEBUG, "Verifying password for sender: "+tic.from);
 		if (!sbbsecho.match_ticpw(tic.from, tic.pw)) {
 			var alink = FIDO.parse_addr(tic.from).toString();
-			log(LOG_INFO, "Verifying password with domain this time: "+alink);
+			log(LOG_DEBUG, "Verifying password with domain this time: "+alink);
 			if (!sbbsecho.match_ticpw(alink, tic.pw)) {
 				// if we get here and there is no match, then we have
 				// defined the wrong password somewhere... - wk42
@@ -677,62 +678,53 @@ function parse_ticfile(fname)
 	return tic;
 }
 
+function import_file_list(dir, list, uploader)
+{
+	log(LOG_INFO, "Importing file list into: " + dir);
+	var fb = new FileBase(dir);
+	if(!fb.open())
+		return "Error " + fb.last_error + " opening filebase: " + dir;
+	for(var i = 0; i < list.length; i++) {
+		var file = list[i];
+		file.from = uploader;
+		log(LOG_INFO, "Adding file (" + file.name + ") to: " + dir);
+		if(!fb.add(file, use_diz_always)) {
+			fb.close();
+			return "Error " + fb.last_error + " adding file to: " + dir;
+		} else
+			files_imported++;
+	}
+	fb.close();
+	return true;
+}
+
 // need the tic for some more processing
 function import_files(tic)
 {
 	log(LOG_INFO, "Importing...");
 	var i;
 	var cmd;
-	var f=new File(system.temp_dir+"tickit-files.bbs");
 
-	for (i in files_bbs) {
+	for (i in file_list) {
 		if (file_area.dir[i] === undefined) {
 			log(LOG_ERROR, "Invalid directory "+i+" when importing!");
 			continue;
 		}
 
-		if (!f.open("wb")) {
-			log(LOG_ERROR, "Unable to create '"+f.name+"'.");
-			return false;
-		}
-		f.write(files_bbs[i]);
-		f.close();
-
 		// figure out the uploader name if there is an override in place
 		// globally or per area. - wk42
 		var uploader = "";
 		var cfg = tickit.acfg[tic.area.toLowerCase()];
 		if (cfg !== undefined && cfg.uploader !== undefined) {
 			uploader = cfg.uploader.toString();
-			log(LOG_INFO, "Using '"+tic.area.toUpperCase()+"' area uploader: "+uploader);
+			log(LOG_DEBUG, "Using '"+tic.area.toUpperCase()+"' area uploader: "+uploader);
 		} else if (tickit.gcfg.uploader !== undefined) {
 			uploader = tickit.gcfg.uploader.toString();
-			log(LOG_INFO, "Using global uploader: "+uploader);
-		}
-		cmd = system.exec_dir+"addfiles "+i;
-		if (uploader !== undefined && uploader !== "")
-			cmd += ' -x "' + uploader + '"';
-		cmd += " -zd +"+f.name+" 24 13";
-		if (tickit.gcfg.addfileslogcap) {
-			// catch addfiles output only if global AddFilesLogCap is
-			// enabled. this is so we can try to determine what causes
-			// addfiles to abort early and not import some files for
-			// some reason. we're going to write this to the
-			// system.logs_dir because system.temp_dir is cleaned
-			// regularly and indiscriminately - wk42
-			if (system.platform === 'Win32')
-				cmd += " 1>>" + system.logs_dir + "addfiles.log 2>&1";
-			else
-				cmd += " >>" + system.logs_dir + "addfiles.log 2>&1";
+			log(LOG_DEBUG, "Using global uploader: "+uploader);
 		}
-		log(LOG_INFO, "Executing: '"+cmd+"'.");
-		log(LOG_INFO, "addfiles returned: "+system.exec(cmd));
+		import_file_list(i, file_list[i], uploader);
 	}
-	// clear the files_bbs array since we're now importing the files one
-	// file at a time as they are processed. this is a quick hack to
-	// enable per area uploader names and i didn't take the time to try
-	// to refactor this properly. it works for now - wk42
-	files_bbs = {};
+	file_list = {};
 }
 
 function main() {
@@ -811,6 +803,8 @@ function main() {
 			}
 		}
 	}
+	if(files_imported > 0)
+		log(LOG_INFO, files_imported + " files imported successfully");
 }
 
 main();
diff --git a/exec/update.js b/exec/update.js
index 5c219aec19183545752517b3a85b207168347014..da49be72b6bc7526e779a3e84424c9c7df6d9b4d 100644
--- a/exec/update.js
+++ b/exec/update.js
@@ -1,8 +1,6 @@
-/* $Id: update.js,v 1.10 2020/05/05 01:09:27 rswindell Exp $ */
+/* Synchronet v3.15+ update script (to be executed with jsexec) */
 
-/* Synchronet v3.15 update script (to be executed with jsexec) */
-
-const REVISION = "$Revision: 1.10 $".split(' ')[1];
+const REVISION = "2.0";
 
 var test = argv.indexOf("-test") >= 0;
 
@@ -150,7 +148,7 @@ function update_gfile_indexes()
 	return count;
 }
 
-printf("Synchronet update.js revision %u\n", REVISION);
+printf("Synchronet update.js revision %s\n", REVISION);
 printf("Updating exec directory: ");
 printf("%s\n", update_exec_dir() ? "Success" : "FAILURE");
 printf("Updating users ip_address field: ");
@@ -207,4 +205,20 @@ for(var i in src_files) {
 	print("Building " + bin);
 	if(!test)
 		system.exec(system.exec_dir + "baja " + src_files[i]);
-}
\ No newline at end of file
+}
+
+print("Checking for v3.19 file bases");
+var upgraded = true;
+for(var d in file_area.dir) {
+	upgraded = false;
+	dir_idx = directory(file_area.dir[d].data_dir + "*.sid");
+	if(dir_idx && dir_idx.length) {
+		upgraded = true;
+		break;
+	}
+}
+if(!upgraded) {
+	var cmdline = system.exec_dir + "upgrade_to_v319";
+	print("No v3.19 file bases found, running " + cmdline);
+	system.exec(cmdline);
+}
diff --git a/exec/updatefiles.js b/exec/updatefiles.js
new file mode 100755
index 0000000000000000000000000000000000000000..4eb370ec3805f0523bcdda69855bf5076e375f6e
--- /dev/null
+++ b/exec/updatefiles.js
@@ -0,0 +1,25 @@
+if(argc < 1) {
+	alert("No directory code specfiied");
+	exit(0);
+}
+var fbase = new FileBase(argv[0]);
+if(!fbase.open()) {
+	alert("failed to open base");
+	exit(1);
+}
+var file_list = fbase.get_list(argv[1] || "*", FileBase.DETAIL.NORM);
+for(var i in file_list) {
+	var file = file_list[i];
+	var copy = JSON.parse(JSON.stringify(file));
+	for(var p in file) {
+		var str = prompt(p + " [" + file[p] + "]");
+		if(str)
+			file[p] = str;
+	}
+	if(JSON.stringify(copy) != JSON.stringify(file)) {
+		alert("changed");
+		print(fbase.update(copy.name, file));
+		print(fbase.status);
+		print(fbase.last_error);
+	}
+}
diff --git a/exec/wildcat.src b/exec/wildcat.src
index 9a9df8698cc002efdfed874e27b44e8d7db5a0d4..80d1c0bc1732ec8caeb177d89da5f5f6f7ca78ee 100644
--- a/exec/wildcat.src
+++ b/exec/wildcat.src
@@ -208,7 +208,7 @@ cmdkey Q
 	end_cmd
 
 cmdkey L
-	setstr "*.*"
+	setstr "*"
 	file_list
         end_cmd
 
diff --git a/src/hash/crc16.c b/src/hash/crc16.c
index 42f848d782aff182c0e51d99212f8c268e0f5bc7..c95801d1252f30d3dcb3f4fb51cec5d29943278c 100644
--- a/src/hash/crc16.c
+++ b/src/hash/crc16.c
@@ -1,44 +1,28 @@
-/* crc16.c */
-
 /* CCITT 16-bit CRC table and calculation function */
 
-/* $Id: crc16.c,v 1.8 2018/07/24 01:12:53 rswindell Exp $ */
-
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
  * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
  *																			*
  * Copyright Rob Swindell - http://www.synchro.net/copyright.html			*
  *																			*
- * This program is free software; you can redistribute it and/or			*
- * modify it under the terms of the GNU General Public License				*
+ * This library is free software; you can redistribute it and/or			*
+ * modify it under the terms of the GNU Lesser General Public License		*
  * as published by the Free Software Foundation; either version 2			*
  * of the License, or (at your option) any later version.					*
- * See the GNU General Public License for more details: gpl.txt or			*
- * http://www.fsf.org/copyleft/gpl.html										*
- *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
+ * See the GNU Lesser General Public License for more details: lgpl.txt or	*
+ * http://www.fsf.org/copyleft/lesser.html									*
  *																			*
  * For Synchronet coding style and modification guidelines, see				*
  * http://www.synchro.net/source.html										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
 #include <string.h>	/* strlen */
 #include "crc16.h"
 
-CRCEXPORT uint16_t crc16tbl[] = {
+uint16_t crc16tbl[] = {
 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7,
 0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF,
 0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6,
@@ -73,16 +57,25 @@ CRCEXPORT uint16_t crc16tbl[] = {
 0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0
 };
 
-uint16_t CRCCALL crc16(const char* data, unsigned long len)
+uint16_t crc16(const char* data, size_t len)
 {
 	uint16_t crc = 0;
-	unsigned long l;
+	size_t l;
 
 	if(len==0 && data!=NULL)
 		len=strlen(data);
 	for(l=0;l<len;l++)
 		crc = ucrc16(data[l],crc);
  
-    return(crc);
+    return crc;
 }
 
+uint16_t icrc16(uint16_t crc, const char* data, size_t len)
+{
+	size_t l;
+
+	for(l=0; l<len; l++)
+		crc = ucrc16(data[l], crc);
+ 
+    return crc;
+}
diff --git a/src/hash/crc16.h b/src/hash/crc16.h
index 0093aa1d5de0cf524e2078b11e36963f1bc6d829..bca8a90eb7b6741eac372948ca52fb2003284b2c 100644
--- a/src/hash/crc16.h
+++ b/src/hash/crc16.h
@@ -1,37 +1,21 @@
-/* crc16.h */
-
 /* CCITT 16-bit CRC table and calculation macro */
 
-/* $Id: crc16.h,v 1.7 2018/07/24 01:12:53 rswindell Exp $ */
-
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
  * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
  *																			*
  * Copyright Rob Swindell - http://www.synchro.net/copyright.html			*
  *																			*
- * This program is free software; you can redistribute it and/or			*
- * modify it under the terms of the GNU General Public License				*
+ * This library is free software; you can redistribute it and/or			*
+ * modify it under the terms of the GNU Lesser General Public License		*
  * as published by the Free Software Foundation; either version 2			*
  * of the License, or (at your option) any later version.					*
- * See the GNU General Public License for more details: gpl.txt or			*
- * http://www.fsf.org/copyleft/gpl.html										*
- *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
+ * See the GNU Lesser General Public License for more details: lgpl.txt or	*
+ * http://www.fsf.org/copyleft/lesser.html									*
  *																			*
  * For Synchronet coding style and modification guidelines, see				*
  * http://www.synchro.net/source.html										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
@@ -39,15 +23,15 @@
 #define _CRC16_H_
 
 #include "gen_defs.h"
-#include "crc32.h"	/* CRCEXPORT/CRCCALL */
 
 #ifdef __cplusplus
 extern "C" {
 #endif
 
-CRCEXPORT extern uint16_t crc16tbl[];
+extern uint16_t crc16tbl[];
 
-CRCEXPORT uint16_t CRCCALL crc16(const char* data, unsigned long len);
+uint16_t crc16(const char* data, size_t len);
+uint16_t icrc16(uint16_t crc, const char* data, size_t len);
 
 #ifdef __cplusplus
 }
diff --git a/src/hash/crc32.c b/src/hash/crc32.c
index 42a7120543befb57f20d7a62af8cde327489ee40..2ad0843043a7f15b7913fe2ab85d0976f0f5a5ad 100644
--- a/src/hash/crc32.c
+++ b/src/hash/crc32.c
@@ -1,44 +1,28 @@
-/* crc32.c */
-
 /* IEEE 802.3 32-bit CRC table and convenience functions */
 
-/* $Id: crc32.c,v 1.12 2018/07/24 01:12:53 rswindell Exp $ */
-
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
  * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
  *																			*
  * Copyright Rob Swindell - http://www.synchro.net/copyright.html			*
  *																			*
- * This program is free software; you can redistribute it and/or			*
- * modify it under the terms of the GNU General Public License				*
+ * This library is free software; you can redistribute it and/or			*
+ * modify it under the terms of the GNU Lesser General Public License		*
  * as published by the Free Software Foundation; either version 2			*
  * of the License, or (at your option) any later version.					*
- * See the GNU General Public License for more details: gpl.txt or			*
- * http://www.fsf.org/copyleft/gpl.html										*
- *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
+ * See the GNU Lesser General Public License for more details: lgpl.txt or	*
+ * http://www.fsf.org/copyleft/lesser.html									*
  *																			*
  * For Synchronet coding style and modification guidelines, see				*
  * http://www.synchro.net/source.html										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
 #include <string.h>	/* strlen */
 #include "crc32.h"
 
-CRCEXPORT int32_t crc32tbl[]={	/* CRC polynomial 0xedb88320 */
+int32_t crc32tbl[]={	/* CRC polynomial 0xedb88320 */
 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3,
 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91,
 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
@@ -78,22 +62,22 @@ CRCEXPORT int32_t crc32tbl[]={	/* CRC polynomial 0xedb88320 */
 /* Pass len of 0 to auto-determine ASCIIZ string length						*/
 /* or non-zero for arbitrary binary data									*/
 /****************************************************************************/
-uint32_t CRCCALL crc32i(uint32_t crc, const char *buf, unsigned long len)
+uint32_t crc32i(uint32_t crc, const char *buf, size_t len)
 {
-	unsigned long l;
+	size_t l;
 
 	if(len==0 && buf!=NULL) 
 		len=strlen(buf);
 	for(l=0;l<len;l++)
 		crc=ucrc32(buf[l],crc);
-	return(~crc);
+	return ~crc;
 }
 
-uint32_t CRCCALL fcrc32(FILE* fp, unsigned long len)
+uint32_t fcrc32(FILE* fp, size_t len)
 {
 	int	ch;
 	uint32_t crc=0xffffffff;
-	unsigned long l;
+	size_t l;
 
 	rewind(fp);
 	for(l=0;(len==0 || l<len) && !feof(fp);l++) {
@@ -101,7 +85,5 @@ uint32_t CRCCALL fcrc32(FILE* fp, unsigned long len)
 			break;
 		crc=ucrc32(ch,crc);
 	}
-	return(~crc);
+	return ~crc;
 }
-
-
diff --git a/src/hash/crc32.h b/src/hash/crc32.h
index 9c6644db56d7415087ad22b29f400bc37f6170b7..b7b7ba49f69c8d587264f0fc9a27e3c5739a1f52 100644
--- a/src/hash/crc32.h
+++ b/src/hash/crc32.h
@@ -1,37 +1,21 @@
-/* crc32.h */
-
 /* 32-bit CRC table and calculation macro */
 
-/* $Id: crc32.h,v 1.18 2019/03/22 21:29:12 rswindell Exp $ */
-
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
  * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
  *																			*
  * Copyright Rob Swindell - http://www.synchro.net/copyright.html			*
  *																			*
- * This program is free software; you can redistribute it and/or			*
- * modify it under the terms of the GNU General Public License				*
+ * This library is free software; you can redistribute it and/or			*
+ * modify it under the terms of the GNU Lesser General Public License		*
  * as published by the Free Software Foundation; either version 2			*
  * of the License, or (at your option) any later version.					*
- * See the GNU General Public License for more details: gpl.txt or			*
- * http://www.fsf.org/copyleft/gpl.html										*
- *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
+ * See the GNU Lesser General Public License for more details: lgpl.txt or	*
+ * http://www.fsf.org/copyleft/lesser.html									*
  *																			*
  * For Synchronet coding style and modification guidelines, see				*
  * http://www.synchro.net/source.html										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
@@ -41,30 +25,14 @@
 #include <stdio.h>	/* FILE */
 #include "gen_defs.h"	/* uint32_t */
 
-#if defined(_WIN32) && (defined(CRC_IMPORTS) || defined(CRC_EXPORTS))
-	#if defined(CRC_IMPORTS)
-		#define CRCEXPORT	__declspec(dllimport)
-	#else
-		#define CRCEXPORT	__declspec(dllexport)
-	#endif
-	#if defined(__BORLANDC__)
-		#define CRCCALL
-	#else
-		#define CRCCALL
-	#endif
-#else	/* !_WIN32 */
-	#define CRCEXPORT
-	#define CRCCALL
-#endif
-
 #ifdef __cplusplus
 extern "C" {
 #endif
 
-CRCEXPORT extern int32_t crc32tbl[];
+extern int32_t crc32tbl[];
 
-CRCEXPORT uint32_t CRCCALL crc32i(uint32_t crc, const char* buf, unsigned long len);
-CRCEXPORT uint32_t CRCCALL fcrc32(FILE* fp, unsigned long len);
+uint32_t crc32i(uint32_t crc, const char* buf, size_t);
+uint32_t fcrc32(FILE* fp, size_t);
 
 #ifdef __cplusplus
 }
diff --git a/src/hash/md5.c b/src/hash/md5.c
index 4359836fa958ab79a0ec72484c6264bc9692ee90..107ce8a8496e521df3957c050d259f3eb6c28dc5 100644
--- a/src/hash/md5.c
+++ b/src/hash/md5.c
@@ -34,7 +34,7 @@ documentation and/or software.
 	#define LITTLE_ENDIAN	/* Little Endian by default */
 #endif
 
-void MD5CALL MD5_open(MD5 *md5)
+void MD5_open(MD5 *md5)
 {
   md5->count[0] = md5->count[1] = 0;
   /* Load magic initialization constants.*/
@@ -194,7 +194,7 @@ static void MD5Transform(uint32_t state[4], const BYTE block[64])
   memset(x, 0, sizeof(x));
 }
 
-void MD5CALL MD5_digest(MD5 *md5, const void *input, size_t inputLen)
+void MD5_digest(MD5 *md5, const void *input, size_t inputLen)
 {
   unsigned int i, index, partLen;
   /* Compute number of bytes mod 64 */
@@ -229,7 +229,7 @@ void MD5CALL MD5_digest(MD5 *md5, const void *input, size_t inputLen)
 #define ENCODE(p,n) (p)[0]=n,(p)[1]=n>>8,(p)[2]=n>>16,(p)[3]=n>>24
 #endif
 
-void MD5CALL MD5_close(MD5 *md5, BYTE digest[MD5_DIGEST_SIZE])
+void MD5_close(MD5 *md5, BYTE digest[MD5_DIGEST_SIZE])
 {
   BYTE bits[8];
   unsigned int index, padLen;
@@ -257,7 +257,7 @@ void MD5CALL MD5_close(MD5 *md5, BYTE digest[MD5_DIGEST_SIZE])
   memset(md5, 0, sizeof(MD5));
 }
 
-BYTE* MD5CALL MD5_calc(BYTE digest[MD5_DIGEST_SIZE], const void* buf, size_t len)
+BYTE* MD5_calc(BYTE digest[MD5_DIGEST_SIZE], const void* buf, size_t len)
 {
 	MD5 ctx;
 
@@ -270,20 +270,20 @@ BYTE* MD5CALL MD5_calc(BYTE digest[MD5_DIGEST_SIZE], const void* buf, size_t len
 
 /* conversion for 16 character binary md5 to hex */
 
-BYTE* MD5CALL MD5_hex(BYTE* to, const BYTE digest[MD5_DIGEST_SIZE])
+char* MD5_hex(char* to, const BYTE digest[MD5_DIGEST_SIZE])
 {
 	BYTE const* from = digest;
-    static char *hexdigits = "0123456789abcdef";
-    const BYTE *end = digest + MD5_DIGEST_SIZE;
-    char *d = (char *)to;
+	const char *hexdigits = "0123456789abcdef";
+	const BYTE *end = digest + MD5_DIGEST_SIZE;
+	char *d = to;
 
-    while (from < end) {
+	while (from < end) {
 		*d++ = hexdigits[(*from >> 4)];
 		*d++ = hexdigits[(*from & 0x0F)];
 		from++;
-    }
-    *d = '\0';
-    return to;
+	}
+	*d = '\0';
+	return to;
 }
 
 #ifdef MD5_TEST
diff --git a/src/hash/md5.h b/src/hash/md5.h
index 2274979df9c46f919af05728f2e911bf5cab8d1c..c23a4a1254b355ff50692d6d9c164c7b2dbc1074 100644
--- a/src/hash/md5.h
+++ b/src/hash/md5.h
@@ -54,26 +54,19 @@ typedef struct
 	#else
 		#define MD5EXPORT	__declspec(dllexport)
 	#endif
-	#if defined(__BORLANDC__)
-		#define MD5CALL
-	#else
-		#define MD5CALL
-	#endif
 #else	/* !_WIN32 */
 	#define MD5EXPORT
-	#define MD5CALL
 #endif
 
-
 #ifdef __cplusplus
 extern "C" {
 #endif
 
-MD5EXPORT void	MD5CALL MD5_open(MD5* ctx);
-MD5EXPORT void	MD5CALL MD5_digest(MD5* ctx, const void* buf, size_t len);
-MD5EXPORT void	MD5CALL MD5_close(MD5* ctx, BYTE digest[MD5_DIGEST_SIZE]);
-MD5EXPORT BYTE*	MD5CALL MD5_calc(BYTE digest[MD5_DIGEST_SIZE], const void* buf, size_t len);
-MD5EXPORT BYTE*	MD5CALL MD5_hex(BYTE* dest, const BYTE digest[MD5_DIGEST_SIZE]);
+MD5EXPORT void	MD5_open(MD5* ctx);
+MD5EXPORT void	MD5_digest(MD5* ctx, const void* buf, size_t len);
+MD5EXPORT void	MD5_close(MD5* ctx, BYTE digest[MD5_DIGEST_SIZE]);
+MD5EXPORT BYTE*	MD5_calc(BYTE digest[MD5_DIGEST_SIZE], const void* buf, size_t len);
+MD5EXPORT char*	MD5_hex(char* dest, const BYTE digest[MD5_DIGEST_SIZE]);
 
 #ifdef __cplusplus
 }
diff --git a/src/hash/objects.mk b/src/hash/objects.mk
index 4447d49073ef4699d27e8d65046570582ad87ad4..e85ac9eb14d8b6b2ea9b4187024a8b14bf2a59d6 100644
--- a/src/hash/objects.mk
+++ b/src/hash/objects.mk
@@ -8,6 +8,6 @@
 
 OBJS	=	$(OBJODIR)$(DIRSEP)crc16$(OFILE) \
 		$(OBJODIR)$(DIRSEP)crc32$(OFILE) \
-		$(OBJODIR)$(DIRSEP)md5$(OFILE)
-
+		$(OBJODIR)$(DIRSEP)md5$(OFILE) \
+		$(OBJODIR)$(DIRSEP)sha1$(OFILE)
 
diff --git a/src/hash/sha1.c b/src/hash/sha1.c
new file mode 100644
index 0000000000000000000000000000000000000000..a9dc3a5e4cef6728031987363a974c3d6a5bf852
--- /dev/null
+++ b/src/hash/sha1.c
@@ -0,0 +1,311 @@
+/*
+SHA-1 in C
+By Steve Reid <steve@edmweb.com>
+100% Public Domain
+
+Test Vectors (from FIPS PUB 180-1)
+"abc"
+  A9993E36 4706816A BA3E2571 7850C26C 9CD0D89D
+"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"
+  84983E44 1C3BD26E BAAE4AA1 F95129E5 E54670F1
+A million repetitions of "a"
+  34AA973C D4C4DAA4 F61EEB2B DBAD2731 6534016F
+*/
+
+/* #define LITTLE_ENDIAN * This should be #define'd already, if true. */
+/* #define SHA1HANDSOFF * Copies data before messing with it. */
+
+#define SHA1HANDSOFF
+
+#include <stdio.h>
+#include <string.h>
+
+/* for uint32_t */
+#include <stdint.h>
+
+#include "sha1.h"
+
+
+#define rol(value, bits) (((value) << (bits)) | ((value) >> (32 - (bits))))
+
+/* blk0() and blk() perform the initial expand. */
+/* I got the idea of expanding during the round function from SSLeay */
+#if BYTE_ORDER == LITTLE_ENDIAN
+#define blk0(i) (block->l[i] = (rol(block->l[i],24)&0xFF00FF00) \
+    |(rol(block->l[i],8)&0x00FF00FF))
+#elif BYTE_ORDER == BIG_ENDIAN
+#define blk0(i) block->l[i]
+#else
+#error "Endianness not defined!"
+#endif
+#define blk(i) (block->l[i&15] = rol(block->l[(i+13)&15]^block->l[(i+8)&15] \
+    ^block->l[(i+2)&15]^block->l[i&15],1))
+
+/* (R0+R1), R2, R3, R4 are the different operations used in SHA1 */
+#define R0(v,w,x,y,z,i) z+=((w&(x^y))^y)+blk0(i)+0x5A827999+rol(v,5);w=rol(w,30);
+#define R1(v,w,x,y,z,i) z+=((w&(x^y))^y)+blk(i)+0x5A827999+rol(v,5);w=rol(w,30);
+#define R2(v,w,x,y,z,i) z+=(w^x^y)+blk(i)+0x6ED9EBA1+rol(v,5);w=rol(w,30);
+#define R3(v,w,x,y,z,i) z+=(((w|x)&y)|(w&x))+blk(i)+0x8F1BBCDC+rol(v,5);w=rol(w,30);
+#define R4(v,w,x,y,z,i) z+=(w^x^y)+blk(i)+0xCA62C1D6+rol(v,5);w=rol(w,30);
+
+
+/* Hash a single 512-bit block. This is the core of the algorithm. */
+
+void SHA1Transform(
+    uint32_t state[5],
+    const uint8_t buffer[64]
+)
+{
+    uint32_t a, b, c, d, e;
+
+    typedef union
+    {
+        uint8_t c[64];
+        uint32_t l[16];
+    } CHAR64LONG16;
+
+#ifdef SHA1HANDSOFF
+    CHAR64LONG16 block[1];      /* use array to appear as a pointer */
+
+    memcpy(block, buffer, 64);
+#else
+    /* The following had better never be used because it causes the
+     * pointer-to-const buffer to be cast into a pointer to non-const.
+     * And the result is written through.  I threw a "const" in, hoping
+     * this will cause a diagnostic.
+     */
+    CHAR64LONG16 *block = (const CHAR64LONG16 *) buffer;
+#endif
+    /* Copy context->state[] to working vars */
+    a = state[0];
+    b = state[1];
+    c = state[2];
+    d = state[3];
+    e = state[4];
+    /* 4 rounds of 20 operations each. Loop unrolled. */
+    R0(a, b, c, d, e, 0);
+    R0(e, a, b, c, d, 1);
+    R0(d, e, a, b, c, 2);
+    R0(c, d, e, a, b, 3);
+    R0(b, c, d, e, a, 4);
+    R0(a, b, c, d, e, 5);
+    R0(e, a, b, c, d, 6);
+    R0(d, e, a, b, c, 7);
+    R0(c, d, e, a, b, 8);
+    R0(b, c, d, e, a, 9);
+    R0(a, b, c, d, e, 10);
+    R0(e, a, b, c, d, 11);
+    R0(d, e, a, b, c, 12);
+    R0(c, d, e, a, b, 13);
+    R0(b, c, d, e, a, 14);
+    R0(a, b, c, d, e, 15);
+    R1(e, a, b, c, d, 16);
+    R1(d, e, a, b, c, 17);
+    R1(c, d, e, a, b, 18);
+    R1(b, c, d, e, a, 19);
+    R2(a, b, c, d, e, 20);
+    R2(e, a, b, c, d, 21);
+    R2(d, e, a, b, c, 22);
+    R2(c, d, e, a, b, 23);
+    R2(b, c, d, e, a, 24);
+    R2(a, b, c, d, e, 25);
+    R2(e, a, b, c, d, 26);
+    R2(d, e, a, b, c, 27);
+    R2(c, d, e, a, b, 28);
+    R2(b, c, d, e, a, 29);
+    R2(a, b, c, d, e, 30);
+    R2(e, a, b, c, d, 31);
+    R2(d, e, a, b, c, 32);
+    R2(c, d, e, a, b, 33);
+    R2(b, c, d, e, a, 34);
+    R2(a, b, c, d, e, 35);
+    R2(e, a, b, c, d, 36);
+    R2(d, e, a, b, c, 37);
+    R2(c, d, e, a, b, 38);
+    R2(b, c, d, e, a, 39);
+    R3(a, b, c, d, e, 40);
+    R3(e, a, b, c, d, 41);
+    R3(d, e, a, b, c, 42);
+    R3(c, d, e, a, b, 43);
+    R3(b, c, d, e, a, 44);
+    R3(a, b, c, d, e, 45);
+    R3(e, a, b, c, d, 46);
+    R3(d, e, a, b, c, 47);
+    R3(c, d, e, a, b, 48);
+    R3(b, c, d, e, a, 49);
+    R3(a, b, c, d, e, 50);
+    R3(e, a, b, c, d, 51);
+    R3(d, e, a, b, c, 52);
+    R3(c, d, e, a, b, 53);
+    R3(b, c, d, e, a, 54);
+    R3(a, b, c, d, e, 55);
+    R3(e, a, b, c, d, 56);
+    R3(d, e, a, b, c, 57);
+    R3(c, d, e, a, b, 58);
+    R3(b, c, d, e, a, 59);
+    R4(a, b, c, d, e, 60);
+    R4(e, a, b, c, d, 61);
+    R4(d, e, a, b, c, 62);
+    R4(c, d, e, a, b, 63);
+    R4(b, c, d, e, a, 64);
+    R4(a, b, c, d, e, 65);
+    R4(e, a, b, c, d, 66);
+    R4(d, e, a, b, c, 67);
+    R4(c, d, e, a, b, 68);
+    R4(b, c, d, e, a, 69);
+    R4(a, b, c, d, e, 70);
+    R4(e, a, b, c, d, 71);
+    R4(d, e, a, b, c, 72);
+    R4(c, d, e, a, b, 73);
+    R4(b, c, d, e, a, 74);
+    R4(a, b, c, d, e, 75);
+    R4(e, a, b, c, d, 76);
+    R4(d, e, a, b, c, 77);
+    R4(c, d, e, a, b, 78);
+    R4(b, c, d, e, a, 79);
+    /* Add the working vars back into context.state[] */
+    state[0] += a;
+    state[1] += b;
+    state[2] += c;
+    state[3] += d;
+    state[4] += e;
+    /* Wipe variables */
+    a = b = c = d = e = 0;
+#ifdef SHA1HANDSOFF
+    memset(block, '\0', sizeof(block));
+#endif
+}
+
+
+/* SHA1Init - Initialize new context */
+
+void SHA1Init(
+    SHA1_CTX * context
+)
+{
+    /* SHA1 initialization constants */
+    context->state[0] = 0x67452301;
+    context->state[1] = 0xEFCDAB89;
+    context->state[2] = 0x98BADCFE;
+    context->state[3] = 0x10325476;
+    context->state[4] = 0xC3D2E1F0;
+    context->count[0] = context->count[1] = 0;
+}
+
+
+/* Run your data through this. */
+
+void SHA1Update(
+    SHA1_CTX * context,
+    const void * buf,
+    size_t len
+)
+{
+    uint32_t i;
+    uint32_t j;
+    uint8_t* data = (uint8_t*)buf;
+
+    j = context->count[0];
+    if ((context->count[0] += len << 3) < j)
+        context->count[1]++;
+    context->count[1] += (len >> 29);
+    j = (j >> 3) & 63;
+    if ((j + len) > 63)
+    {
+        memcpy(&context->buffer[j], data, (i = 64 - j));
+        SHA1Transform(context->state, context->buffer);
+        for (; i + 63 < len; i += 64)
+        {
+            SHA1Transform(context->state, &data[i]);
+        }
+        j = 0;
+    }
+    else
+        i = 0;
+    memcpy(&context->buffer[j], &data[i], len - i);
+}
+
+
+/* Add padding and return the message digest. */
+
+void SHA1Final(
+    SHA1_CTX * context,
+    uint8_t digest[SHA1_DIGEST_SIZE]
+)
+{
+    unsigned i;
+
+    uint8_t finalcount[8];
+
+    uint8_t c;
+
+#if 0    /* untested "improvement" by DHR */
+    /* Convert context->count to a sequence of bytes
+     * in finalcount.  Second element first, but
+     * big-endian order within element.
+     * But we do it all backwards.
+     */
+    uint8_t *fcp = &finalcount[8];
+
+    for (i = 0; i < 2; i++)
+    {
+        uint32_t t = context->count[i];
+
+        int j;
+
+        for (j = 0; j < 4; t >>= 8, j++)
+            *--fcp = (uint8_t) t}
+#else
+    for (i = 0; i < 8; i++)
+    {
+        finalcount[i] = (uint8_t) ((context->count[(i >= 4 ? 0 : 1)] >> ((3 - (i & 3)) * 8)) & 255);      /* Endian independent */
+    }
+#endif
+    c = 0200;
+    SHA1Update(context, &c, 1);
+    while ((context->count[0] & 504) != 448)
+    {
+        c = 0000;
+        SHA1Update(context, &c, 1);
+    }
+    SHA1Update(context, finalcount, 8); /* Should cause a SHA1Transform() */
+    for (i = 0; i < 20; i++)
+    {
+        digest[i] = (uint8_t)
+            ((context->state[i >> 2] >> ((3 - (i & 3)) * 8)) & 255);
+    }
+    /* Wipe variables */
+    memset(context, '\0', sizeof(*context));
+    memset(&finalcount, '\0', sizeof(finalcount));
+}
+
+void SHA1_calc(
+    uint8_t *hash_out,
+    const void *data,
+    size_t len)
+{
+    SHA1_CTX ctx;
+    unsigned int ii;
+
+    SHA1Init(&ctx);
+    for (ii=0; ii<len; ii+=1)
+        SHA1Update(&ctx, (const char*)data + ii, 1);
+    SHA1Final(&ctx, hash_out);
+}
+
+/* conversion for 20 byte binary sha1 to hex */
+char* SHA1_hex(char* to, const uint8_t digest[SHA1_DIGEST_SIZE])
+{
+	uint8_t const* from = digest;
+	const char *hexdigits = "0123456789abcdef";
+	const uint8_t *end = digest + SHA1_DIGEST_SIZE;
+	char *d = to;
+
+	while (from < end) {
+		*d++ = hexdigits[(*from >> 4)];
+		*d++ = hexdigits[(*from & 0x0F)];
+		from++;
+	}
+	*d = '\0';
+	return to;
+}
diff --git a/src/hash/sha1.h b/src/hash/sha1.h
new file mode 100644
index 0000000000000000000000000000000000000000..852585ca2bd9638f82ec26dfd0db4718c21cd508
--- /dev/null
+++ b/src/hash/sha1.h
@@ -0,0 +1,57 @@
+#ifndef SHA1_H
+#define SHA1_H
+
+/*
+   SHA-1 in C
+   By Steve Reid <steve@edmweb.com>
+   100% Public Domain
+ */
+
+#include <stddef.h>		/* size_t */
+#include <gen_defs.h>	/* uint32_t */
+
+#define SHA1_DIGEST_SIZE 20
+
+typedef struct
+{
+    uint32_t state[5];
+    uint32_t count[2];
+    uint8_t buffer[64];
+} SHA1_CTX;
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+void SHA1Transform(
+    uint32_t state[5],
+    const uint8_t buffer[64]
+    );
+
+void SHA1Init(
+    SHA1_CTX * context
+    );
+
+void SHA1Update(
+    SHA1_CTX * context,
+    const void * data,
+    size_t len
+    );
+
+void SHA1Final(
+    SHA1_CTX * context,
+    uint8_t digest[SHA1_DIGEST_SIZE]
+    );
+
+void SHA1_calc(
+    uint8_t *hash_out,
+    const void *str,
+    size_t len);
+
+char* SHA1_hex(char* to, const uint8_t digest[SHA1_DIGEST_SIZE]);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* SHA1_H */
diff --git a/src/sbbs3/GNUmakefile b/src/sbbs3/GNUmakefile
index 5c149e77d75a3632700b22b2b0b86227d1847b02..d4bb30124ee7be91e2ea6827046505e1e8376899 100644
--- a/src/sbbs3/GNUmakefile
+++ b/src/sbbs3/GNUmakefile
@@ -20,6 +20,7 @@ UTIL_LDFLAGS	:=	$(LDFLAGS)
 UTIL_LDFLAGS	+=	$(SMBLIB_LDFLAGS) $(UIFC-MT_LDFLAGS) $(CIOLIB-MT_LDFLAGS) $(XPDEV_LDFLAGS) $(ENCODE_LDFLAGS)
 CONSOLE_LDFLAGS	+=	$(LDFLAGS) $(SMBLIB_LDFLAGS) $(XPDEV_LDFLAGS)
 UTIL_LIBS	+=	$(HASH_LIBS)
+FILE_LIBS	= -larchive
 
 ifndef bcc
  ifneq ($(os),sunos)
@@ -90,12 +91,12 @@ SHLIBOPTS	:=	-shared
 ifeq ($(os),darwin)
  MKSHLIB		:=	libtool -dynamic -framework System -lcc_dynamic
  MKSHPPLIB		:=	libtool -dynamic -framework System -lcc_dynamic -lstdc++
- SHLIBOPTS	:=	
+ SHLIBOPTS	:=
 else
  ifeq ($(os),sunos)
   MKSHLIB		:=	/usr/ccs/bin/ld -G
   MKSHPPLIB		:=	/usr/ccs/bin/ld -G
-  SHLIBOPTS	:=	
+  SHLIBOPTS	:=
  else
   MKSHLIB		:=	$(CC)
   MKSHPPLIB		:=	$(CXX)
@@ -122,7 +123,7 @@ $(SBBSMONO): $(MONO_OBJS) $(OBJS)
 # Synchronet BBS library Link Rule
 $(SBBS): $(JS_DEPS) $(CRYPT_DEPS) $(OBJS) $(LIBS) $(EXTRA_SBBS_DEPENDS) $(ENCODE_LIB) $(HASH_LIB) | $(LIBODIR)
 	@echo Linking $@
-	$(QUIET)$(MKSHPPLIB) $(LDFLAGS) -o $@ $(OBJS) $(SBBS_LIBS) $(SMBLIB_LIBS) $(LIBS) $(SHLIBOPTS) $(JS_LIBS) $(CRYPT_LIBS) $(ENCODE_LIBS) $(HASH_LIBS) $(XPDEV-MT_LIBS)
+	$(QUIET)$(MKSHPPLIB) $(LDFLAGS) -o $@ $(OBJS) $(SBBS_LIBS) $(SMBLIB_LIBS) $(LIBS) $(SHLIBOPTS) $(JS_LIBS) $(CRYPT_LIBS) $(ENCODE_LIBS) $(HASH_LIBS) $(XPDEV-MT_LIBS) $(FILE_LIBS)
 
 # FTP Server Link Rule
 $(FTPSRVR): $(MTOBJODIR)/ftpsrvr.o
@@ -182,7 +183,7 @@ $(SMBUTIL): $(SMBUTIL_OBJS)
 # SBBSecho (FidoNet Packet Tosser)
 $(SBBSECHO): $(SBBSECHO_OBJS)
 	@echo Linking $@
-	$(QUIET)$(CC) $(CONSOLE_LDFLAGS) -o $@ $(SBBSECHO_OBJS) $(SMBLIB_LIBS) $(XPDEV_LIBS) $(ENCODE_LIBS) $(HASH_LIBS)
+	$(QUIET)$(CC) $(CONSOLE_LDFLAGS) -o $@ $(SBBSECHO_OBJS) $(SMBLIB_LIBS) $(XPDEV_LIBS) $(ENCODE_LIBS) $(HASH_LIBS) $(FILE_LIBS)
 
 # SBBSecho Configuration Program
 $(ECHOCFG): $(ECHOCFG_OBJS) $(ENCODE_LIB)
@@ -192,12 +193,12 @@ $(ECHOCFG): $(ECHOCFG_OBJS) $(ENCODE_LIB)
 # ADDFILES
 $(ADDFILES): $(ADDFILES_OBJS)
 	@echo Linking $@
-	$(QUIET)$(CC) $(CONSOLE_LDFLAGS) -o $@ $(ADDFILES_OBJS) $(XPDEV_LIBS) $(SMBLIB_LIBS) $(ENCODE_LIBS) $(HASH_LIBS)
+	$(QUIET)$(CC) $(CONSOLE_LDFLAGS) -o $@ $(ADDFILES_OBJS) $(XPDEV_LIBS) $(SMBLIB_LIBS) $(ENCODE_LIBS) $(HASH_LIBS) $(FILE_LIBS)
 
 # FILELIST
 $(FILELIST): $(FILELIST_OBJS) $(ENCODE_LIB)
 	@echo Linking $@
-	$(QUIET)$(CC) $(CONSOLE_LDFLAGS) -o $@ $(FILELIST_OBJS) $(XPDEV_LIBS) $(ENCODE_LIBS)
+	$(QUIET)$(CC) $(CONSOLE_LDFLAGS) -o $@ $(FILELIST_OBJS) $(XPDEV_LIBS) $(SMBLIB_LIBS) $(ENCODE_LIBS) $(HASH_LIBS) $(FILE_LIBS)
 
 # MAKEUSER
 $(MAKEUSER): $(MAKEUSER_OBJS)
@@ -207,7 +208,7 @@ $(MAKEUSER): $(MAKEUSER_OBJS)
 # JSDOOR
 $(JSDOOR): $(JSDOOR_OBJS) $(XPDEV_LIB) $(ENCODE_LIB) $(HASH_LIB) | $(EXEODIR)
 	@echo Linking $@
-	$(QUIET)$(CXX) $(JS_CFLAGS) $(LDFLAGS) $(MT_LDFLAGS) -o $@ $(JSDOOR_OBJS) $(JS_LIBS) $(CRYPT_LIBS) $(UIFC-MT_LIBS) $(CIOLIB-MT_LIBS) $(SMBLIB_LIBS) $(ENCODE_LIBS) $(XPDEV-MT_LIBS) $(HASH_LIBS)
+	$(QUIET)$(CXX) $(JS_CFLAGS) $(LDFLAGS) $(MT_LDFLAGS) -o $@ $(JSDOOR_OBJS) $(JS_LIBS) $(CRYPT_LIBS) $(UIFC-MT_LIBS) $(CIOLIB-MT_LIBS) $(SMBLIB_LIBS) $(ENCODE_LIBS) $(XPDEV-MT_LIBS) $(HASH_LIBS) $(FILE_LIBS)
 
 # JSEXEC
 $(JSEXEC): $(JSEXEC_OBJS) $(SBBS)
@@ -247,12 +248,12 @@ $(ALLUSERS): $(ALLUSERS_OBJS) $(ENCODE_LIB)
 # DELFILES
 $(DELFILES): $(DELFILES_OBJS) $(ENCODE_LIB)
 	@echo Linking $@
-	$(QUIET)$(CC) $(CONSOLE_LDFLAGS) -o $@ $(DELFILES_OBJS) $(XPDEV_LIBS) $(ENCODE_LIBS)
+	$(QUIET)$(CC) $(CONSOLE_LDFLAGS) -o $@ $(DELFILES_OBJS) $(SMBLIB_LIBS) $(XPDEV_LIBS) $(ENCODE_LIBS) $(HASH_LIBS) $(FILE_LIBS)
 
 # DUPEFIND
 $(DUPEFIND): $(DUPEFIND_OBJS) $(ENCODE_LIB)
 	@echo Linking $@
-	$(QUIET)$(CC) $(CONSOLE_LDFLAGS) -o $@ $(DUPEFIND_OBJS) $(HASH_LIBS) $(XPDEV_LIBS) $(ENCODE_LIBS)
+	$(QUIET)$(CC) $(CONSOLE_LDFLAGS) -o $@ $(DUPEFIND_OBJS) $(SMBLIB_LIBS) $(HASH_LIBS) $(XPDEV_LIBS) $(ENCODE_LIBS)
 
 # SMBACTIV
 $(SMBACTIV): $(SMBACTIV_OBJS)
@@ -283,3 +284,7 @@ $(PKTDUMP): $(PKTDUMP_OBJS) $(OBJODIR) $(EXEODIR)
 $(FMSGDUMP): $(FMSGDUMP_OBJS) $(OBJODIR) $(EXEODIR)
 	@echo Linking $@
 	$(QUIET)$(CC) $(CONSOLE_LDFLAGS) -o $@ $(FMSGDUMP_OBJS)
+
+$(UPGRADE_TO_V319): $(UPGRADE_TO_V319_OBJS) $(OBJODIR) $(EXEODIR)
+	@echo Linking $@
+	$(QUIET)$(CC) $(CONSOLE_LDFLAGS) -o $@ $(UPGRADE_TO_V319_OBJS) $(SMBLIB_LIBS) $(XPDEV_LIBS) $(ENCODE_LIBS) $(HASH_LIBS) $(FILE_LIBS)
diff --git a/src/sbbs3/addfiles.c b/src/sbbs3/addfiles.c
index a79d406def0039ac8049244f29d9f8d3397b3e9b..a6da5a368ceda9c4690ce7dd0ebfca9029fc1de8 100644
--- a/src/sbbs3/addfiles.c
+++ b/src/sbbs3/addfiles.c
@@ -1,4 +1,4 @@
-/* Program to add files to a Synchronet file database */
+/* Add files to a Synchronet file database(s) */
 
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
@@ -26,10 +26,14 @@
 #include "userdat.h"
 #include "filedat.h"
 #include "load_cfg.h"
+#include "smblib.h"
+#include "git_branch.h"
+#include "git_hash.h"
+
 #include <stdbool.h>
 #include <stdarg.h>
 
-#define ADDFILES_VER "3.05"
+#define ADDFILES_VER "3.19"
 
 scfg_t scfg;
 
@@ -74,103 +78,6 @@ int lprintf(int level, const char *fmat, ...)
 	return(chcount);
 }
 
-void prep_desc(char *str)
-{
-	char tmp[1024];
-	int i,j;
-
-	for(i=j=0;str[i] && j < sizeof(tmp)-1;i++) {
-		if(j && str[i]==' ' && tmp[j-1]==' ' && (mode&KEEP_SPACE))
-			tmp[j++]=str[i];
-		else if(j && str[i]<=' ' && str[i] > 0&& tmp[j-1]==' ')
-			continue;
-		else if(i && !IS_ALPHANUMERIC(str[i]) && str[i]==str[i-1])
-			continue;
-		else if(str[i]>=' ' || str[i]<0)
-			tmp[j++]=str[i];
-		else if(str[i]==TAB || (str[i]==CR && str[i+1]==LF))
-			tmp[j++]=' ';
-	}
-	tmp[j]=0;
-	strcpy(str,tmp);
-}
-
-/*****************************************************************************/
-/* Returns command line generated from instr with %c replacments             */
-/*****************************************************************************/
-char *mycmdstr(const char *instr, const char *fpath, const char *fspec, char *outstr)
-{
-    static char cmd[MAX_PATH+1];
-    char str[MAX_PATH+1];
-    int i,j,len;
-#ifdef _WIN32
-	char sfpath[MAX_PATH+1];
-#endif
-
-	len=strlen(instr);
-	for(i=j=0;i<len && j<128;i++) {
-		if(instr[i]=='%') {
-			i++;
-			cmd[j]=0;
-			switch(toupper(instr[i])) {
-				case 'F':   /* File path */
-					SAFECAT(cmd,fpath);
-					break;
-				case '~':	/* DOS-compatible (8.3) filename */
-#ifdef _WIN32
-					SAFECOPY(sfpath,fpath);
-					GetShortPathName(fpath,sfpath,sizeof(sfpath));
-					SAFECAT(cmd,sfpath);
-#else
-                    SAFECAT(cmd,fpath);
-#endif
-					break;
-				case 'G':   /* Temp directory */
-					SAFECAT(cmd,scfg.temp_dir);
-					break;
-                case 'J':
-                    SAFECAT(cmd,scfg.data_dir);
-                    break;
-                case 'K':
-                    SAFECAT(cmd,scfg.ctrl_dir);
-                    break;
-				case 'N':   /* Node Directory (same as SBBSNODE environment var) */
-					SAFECAT(cmd,scfg.node_dir);
-					break;
-				case 'S':   /* File Spec */
-					SAFECAT(cmd,fspec);
-					break;
-                case 'Z':
-                    SAFECAT(cmd,scfg.text_dir);
-                    break;
-				case '!':   /* EXEC Directory */
-					SAFECAT(cmd,scfg.exec_dir);
-					break;
-                case '@':   /* EXEC Directory for DOS/OS2/Win32, blank for Unix */
-#ifndef __unix__
-                    SAFECAT(cmd,scfg.exec_dir);
-#endif
-                    break;
-				case '#':   /* Node number (same as SBBSNNUM environment var) */
-					sprintf(str,"%d",scfg.node_num);
-					SAFECAT(cmd,str);
-					break;
-				case '%':   /* %% for percent sign */
-					SAFECAT(cmd,"%");
-					break;
-				default:    /* unknown specification */
-					break;
-			}
-			j=strlen(cmd);
-		}
-		else
-			cmd[j++]=instr[i];
-	}
-	cmd[j]=0;
-
-	return(cmd);
-}
-
 /****************************************************************************/
 /* Updates dstst.dab file													*/
 /****************************************************************************/
@@ -197,62 +104,44 @@ void updatestats(ulong size)
 	close(file);
 }
 
-bool get_file_diz(file_t* f, const char* filepath, char* ext)
+bool reupload(smb_t* smb, file_t* f)
 {
-	int i,file;
-	char tmp[MAX_PATH+1];
-	char tmpext[513];
-
-	for(i=0;i<scfg.total_fextrs;i++)
-		if(!stricmp(scfg.fextr[i]->ext,f->name+9)
-			&& chk_ar(&scfg,scfg.fextr[i]->ar,/* user: */NULL, /* client: */NULL))
-			break;
-	// If we could not find an extractor which matches our requirements, use any
-	if(i >= scfg.total_fextrs) {
-		for(i=0;i<scfg.total_fextrs;i++)
-			if(!stricmp(scfg.fextr[i]->ext,f->name+9))
-				break;
-	}
-	if(i >= scfg.total_fextrs)
+	char path[MAX_PATH + 1];
+	if(!smb_renewfile(smb, f, SMB_SELFPACK, getfilepath(&scfg, f, path))) {
+		fprintf(stderr, "!Error renewing: %s\n", f->name);
 		return false;
-
-	SAFEPRINTF(tmp,"%sFILE_ID.DIZ",scfg.temp_dir);
-	removecase(tmp);
-	system(mycmdstr(scfg.fextr[i]->cmd,filepath,"FILE_ID.DIZ",NULL));
-	if(!fexistcase(tmp)) {
-		SAFEPRINTF(tmp,"%sDESC.SDI",scfg.temp_dir);
-		removecase(tmp);
-		system(mycmdstr(scfg.fextr[i]->cmd,filepath,"DESC.SDI",NULL));
-		fexistcase(tmp);
 	}
+	return true;
+}
+
 
-	if((file=nopen(tmp,O_RDONLY|O_BINARY)) == -1)
+bool get_file_diz(file_t* f, char* ext, size_t maxlen)
+{
+	char path[MAX_PATH + 1];
+	printf("Extracting DIZ from: %s\n", getfilepath(&scfg, f, path));
+	char diz_fpath[MAX_PATH + 1];
+	if(!extract_diz(&scfg, f, /* diz_fnames */NULL, diz_fpath, sizeof(diz_fpath))) {
+		printf("DIZ does not exist in: %s\n", getfilepath(&scfg, f, path));
 		return false;
+	}
+	printf("Parsing DIZ: %s\n", diz_fpath);
+	str_list_t lines = read_diz(diz_fpath, /* max_line_len: */80);
+	format_diz(lines, ext, maxlen, /* allow_ansi: */false);
+	strListFree(&lines);
+	remove(diz_fpath);
 
-	memset(ext,0,513);
-	read(file,ext,512);
-	for(i=512;i;i--)
-		if(ext[i-1]>' ' || ext[i-1]<0)
-			break;
-	ext[i]=0;
 	if(mode&ASCII_ONLY)
 		strip_exascii(ext, ext);
+
 	if(!(mode&KEEP_DESC)) {
-		sprintf(tmpext,"%.256s",ext);
-		prep_desc(tmpext);
-		for(i=0;tmpext[i];i++)
-			if(IS_ALPHA(tmpext[i]))
-				break;
-		sprintf(f->desc,"%.*s",LEN_FDESC,tmpext+i);
-		for(i=0;(f->desc[i]>=' ' || f->desc[i]<0) && i<LEN_FDESC;i++)
-			;
-		f->desc[i]=0; }
-	close(file);
-	f->misc|=FM_EXTDESC;
+		char* fdesc = strdup(ext);
+		smb_new_hfield_str(f, SMB_FILEDESC, prep_file_desc(fdesc, fdesc));
+		free(fdesc);
+	}
 	return true;
 }
 
-void addlist(char *inpath, file_t f, uint dskip, uint sskip)
+void addlist(char *inpath, uint dirnum, const char* uploader, uint dskip, uint sskip)
 {
 	char str[MAX_PATH+1];
 	char tmp[MAX_PATH+1];
@@ -263,53 +152,54 @@ void addlist(char *inpath, file_t f, uint dskip, uint sskip)
 	char *p;
 	char ext[1024];
 	int i;
-	long l;
+	off_t l;
 	BOOL exist;
 	FILE *stream;
 	DIR*	dir;
 	DIRENT*	dirent;
+	smb_t smb;
+	file_t f;
 
+	int result = smb_open_dir(&scfg, &smb, dirnum);
+	if(result != SMB_SUCCESS) {
+		fprintf(stderr, "!Error %d (%s) opening %s\n", result, smb.last_error, smb.file);
+		return;
+	}
+	str_list_t fname_list = loadfilenames(&smb, ALLFILES, /* time: */0, FILE_SORT_NATURAL, /* count: */NULL);
 	if(mode&SEARCH_DIR) {
-		SAFECOPY(str,cur_altpath ? scfg.altpath[cur_altpath-1] : scfg.dir[f.dir]->path);
+		SAFECOPY(str,cur_altpath ? scfg.altpath[cur_altpath-1] : scfg.dir[dirnum]->path);
 		printf("Searching %s\n\n",str);
 		dir=opendir(str);
 
 		while(dir!=NULL && (dirent=readdir(dir))!=NULL) {
-			sprintf(tmp,"%s%s"
-				,cur_altpath ? scfg.altpath[cur_altpath-1] : scfg.dir[f.dir]->path
+			sprintf(filepath, "%s%s"
+				,cur_altpath ? scfg.altpath[cur_altpath-1] : scfg.dir[dirnum]->path
 				,dirent->d_name);
-			if(isdir(tmp))
+			if(isdir(filepath))
 				continue;
-#ifdef _WIN32
-			GetShortPathName(tmp, filepath, sizeof(filepath));
-#else
-			SAFECOPY(filepath,tmp);
-#endif
-			f.misc=0;
-			f.desc[0]=0;
+			const char* fname = getfname(filepath);
+			printf("%s  ", fname);
+			if(strListFind(fname_list, fname, /* case-sensitive: */FALSE) && (mode&NO_UPDATE)) {
+				printf("already added.\n");
+				continue;
+			}
 			memset(ext, 0, sizeof(ext));
-			f.cdt=flength(filepath);
+			memset(&f, 0, sizeof(f));
+			char fdesc[LEN_FDESC + 1] = {0};
+			uint32_t cdt = (uint32_t)flength(filepath);
 			time_t file_timestamp = fdate(filepath);
-			padfname(getfname(filepath),f.name);
-			printf("%s  %10"PRIu32"  %s\n"
-				,f.name,f.cdt,unixtodstr(&scfg,(time32_t)file_timestamp,str));
-			exist=findfile(&scfg,f.dir,f.name);
+			printf("%10"PRIu32"  %s\n"
+				,cdt, unixtodstr(&scfg,(time32_t)file_timestamp,str));
+			exist = smb_findfile(&smb, fname, &f) == SMB_SUCCESS;
 			if(exist) {
 				if(mode&NO_UPDATE)
 					continue;
-				if(!getfileixb(&scfg,&f)) {
-					fprintf(stderr, "!ERROR reading index of directory %u\n", f.dir);
-					continue;
-				}
-				if((mode&CHECK_DATE) && file_timestamp <= f.dateuled)
+				if((mode&CHECK_DATE) && file_timestamp <= f.idx.time)
 					continue;
 				if(mode&ULDATE_ONLY) {
-					f.dateuled=time32(NULL);
-					update_uldate(&scfg, &f);
-					continue;
-				}
-				if(f.misc & FM_EXTDESC)
-					getextdesc(&scfg, f.dir, f.datoffset, ext);
+					reupload(&smb, &f);
+					continue; 
+				} 
 			}
 
 			if(mode&TODAYS_DATE) {		/* put today's date in desc */
@@ -320,7 +210,7 @@ void addlist(char *inpath, file_t f, uint dskip, uint sskip)
 					strftime(f.desc, sizeof(f.desc), datefmt, &tm);
 				} else
 					unixtodstr(&scfg, (time32_t)now, f.desc);
-				SAFECAT(f.desc,"  ");
+				SAFECAT(fdesc,"  ");
 			}
 			else if(mode&FILE_DATE) {		/* get the file date and put into desc */
 				if(datefmt) {
@@ -329,35 +219,45 @@ void addlist(char *inpath, file_t f, uint dskip, uint sskip)
 					strftime(f.desc, sizeof(f.desc), datefmt, &tm);
 				} else
 					unixtodstr(&scfg,(time32_t)file_timestamp,f.desc);
-				SAFECAT(f.desc,"  ");
+				SAFECAT(fdesc,"  ");
 			}
 
-			if(mode&FILE_ID)
-				get_file_diz(&f, filepath, ext);
+			char* ext_desc = NULL;
+			if(mode&FILE_ID) {
+				if(get_file_diz(&f, ext, sizeof(ext))) {
+					ext_desc = ext;
+				}
+			}
 
-			f.dateuled=time32(NULL);
-			f.altpath=cur_altpath;
-			prep_desc(f.desc);
+			prep_file_desc(fdesc, fdesc);
 			if(mode&ASCII_ONLY)
-				strip_exascii(f.desc, f.desc);
+				strip_exascii(fdesc, fdesc);
+
+			smb_hfield_str(&f, SMB_FILENAME, fname);
+			smb_hfield_str(&f, SMB_FILEDESC, fdesc);
+			smb_hfield_str(&f, SENDER, uploader);
+			smb_hfield_bin(&f, SMB_COST, cdt);
+
+			truncsp(ext_desc);
 			if(exist) {
-				putfiledat(&scfg,&f);
+				result = smb_updatemsg(&smb, &f);
 				if(!(mode&NO_NEWDATE))
-					update_uldate(&scfg, &f);
+					reupload(&smb, &f);
 			}
 			else
-				addfiledat(&scfg,&f);
-			if(f.misc&FM_EXTDESC) {
-				truncsp(ext);
-				putextdesc(&scfg,f.dir,f.datoffset,ext);
-			}
+				result = smb_addfile(&smb, &f, SMB_SELFPACK, ext_desc, filepath);
+			smb_freefilemem(&f);
+			if(result != SMB_SUCCESS)
+				fprintf(stderr, "!Error %d (%s) adding file to %s", result, smb.last_error, smb.file);
 			if(mode&UL_STATS)
-				updatestats(f.cdt);
-			files++;
+				updatestats(cdt);
+			files++; 
 		}
 		if(dir!=NULL)
 			closedir(dir);
-		return;
+		smb_close(&smb);
+		strListFree(&fname_list);
+		return; 
 	}
 
 
@@ -367,22 +267,23 @@ void addlist(char *inpath, file_t f, uint dskip, uint sskip)
 		fprintf(stderr,"Error %d (%s) opening %s\n"
 			,errno,strerror(errno),listpath);
 		sprintf(listpath,"%s%s",cur_altpath ? scfg.altpath[cur_altpath-1]
-				: scfg.dir[f.dir]->path,inpath);
+				: scfg.dir[dirnum]->path,inpath);
 		fexistcase(listpath);
 		if((stream=fopen(listpath,"r"))==NULL) {
 			printf("Can't open: %s\n"
 				   "        or: %s\n",inpath,listpath);
-			return;
-		}
+			smb_close(&smb);
+			strListFree(&fname_list);
+			return; 
+		} 
 	}
 
 	printf("Adding %s to %s %s\n\n"
-		,listpath,scfg.lib[scfg.dir[f.dir]->lib]->sname,scfg.dir[f.dir]->sname);
+		,listpath,scfg.lib[scfg.dir[dirnum]->lib]->sname,scfg.dir[dirnum]->sname);
 
 	fgets(nextline,255,stream);
 	do {
-		f.misc=0;
-		f.desc[0]=0;
+		char fdesc[LEN_FDESC + 1] = {0};
 		memset(ext, 0, sizeof(ext));
 		SAFECOPY(curline,nextline);
 		nextline[0]=0;
@@ -393,11 +294,6 @@ void addlist(char *inpath, file_t f, uint dskip, uint sskip)
 		printf("%s\n",curline);
 		SAFECOPY(fname,curline);
 
-#if 0	/* Files without dots are valid on modern systems */
-		p=strchr(fname,'.');
-		if(!p || p==fname || p>fname+8)    /* no dot or invalid dot location */
-			continue;
-#endif
 		p=strchr(fname,' ');
 		if(p) *p=0;
 #if 0 // allow import of bare filename list
@@ -407,68 +303,37 @@ void addlist(char *inpath, file_t f, uint dskip, uint sskip)
 		if(!IS_ALPHANUMERIC(*fname)) {	// filename doesn't begin with an alpha-numeric char?
 			continue;
 		}
-		SAFEPRINTF2(filepath,"%s%s",cur_altpath ? scfg.altpath[cur_altpath-1]
-			: scfg.dir[f.dir]->path,fname);
-
-#ifdef _WIN32
-		{
-			char shortpath[MAX_PATH+1];
-			GetShortPathName(filepath, shortpath, sizeof(shortpath));
-			SAFECOPY(fname, getfname(shortpath));
-		}
-#else
-		fexistcase(filepath);
-		SAFECOPY(fname, getfname(filepath));
-#endif
 
-		padfname(fname,f.name);
-		if(strcspn(f.name,"\\/|<>+[]:=\";,")!=strlen(f.name))
-			continue;
-
-		for(i=0;i<12;i++)
-			if(f.name[i]<' ' || (mode&ASCII_ONLY && (uchar)f.name[i]>0x7e))
-				break;
+		sprintf(filepath, "%s%s", cur_altpath ? scfg.altpath[cur_altpath-1]
+			: scfg.dir[dirnum]->path,fname);
 
-		if(i<12)					/* Ctrl chars or EX-ASCII in filename? */
+		if(strcspn(fname,"\\/|<>+[]:=\";,")!=strlen(fname)) {
+			fprintf(stderr, "!Illegal filename: %s\n", fname);
 			continue;
+		}
+
 		time_t file_timestamp = fdate(filepath);
-		exist=findfile(&scfg,f.dir,f.name);
+		exist = smb_findfile(&smb, fname, &f) == SMB_SUCCESS;
 		if(exist) {
 			if(mode&NO_UPDATE)
 				continue;
-			if(!getfileixb(&scfg,&f)) {
-				fprintf(stderr, "!ERROR reading index of directory %u\n", f.dir);
-				continue;
-			}
-			if((mode&CHECK_DATE) && file_timestamp <= f.dateuled)
+			if((mode&CHECK_DATE) && file_timestamp <= f.idx.time)
 				continue;
 			if(mode&ULDATE_ONLY) {
-				f.dateuled=time32(NULL);
-				update_uldate(&scfg, &f);
-				continue;
-			}
-			if (f.misc & FM_EXTDESC)
-				getextdesc(&scfg, f.dir, f.datoffset, ext);
+				reupload(&smb, &f);
+				continue; 
+			} 
 		}
 
-		if(mode&TODAYS_DATE) {		/* put today's date in desc */
-			time_t now = time(NULL);
-			if(datefmt) {
-				struct tm tm = {0};
-				localtime_r(&now, &tm);
-				strftime(f.desc, sizeof(f.desc), datefmt, &tm);
-			} else
-				unixtodstr(&scfg, (time32_t)now, f.desc);
-			SAFECAT(f.desc,"  ");
+		if(mode&FILE_DATE) {		/* get the file date and put into desc */
+			unixtodstr(&scfg,(time32_t)file_timestamp, fdesc);
+			strcat(fdesc, "  "); 
 		}
-		else if(mode&FILE_DATE) {		/* get the file date and put into desc */
-			if(datefmt) {
-				struct tm tm = {0};
-				localtime_r(&file_timestamp, &tm);
-				strftime(f.desc, sizeof(f.desc), datefmt, &tm);
-			} else
-				unixtodstr(&scfg,(time32_t)file_timestamp,f.desc);
-			SAFECAT(f.desc,"  ");
+
+		if(mode&TODAYS_DATE) {		/* put today's date in desc */
+			l=time32(NULL);
+			unixtodstr(&scfg,(time32_t)l,fdesc);
+			strcat(fdesc, "  "); 
 		}
 
 		if(dskip && strlen(curline)>=dskip) p=curline+dskip;
@@ -478,21 +343,22 @@ void addlist(char *inpath, file_t f, uint dskip, uint sskip)
 			SKIP_WHITESPACE(p);
 		}
 		SAFECOPY(tmp,p);
-		prep_desc(tmp);
-		sprintf(f.desc+strlen(f.desc),"%.*s",(int)(LEN_FDESC-strlen(f.desc)),tmp);
+		prep_file_desc(tmp, tmp);
+		sprintf(fdesc + strlen(fdesc), "%.*s", (int)(LEN_FDESC-strlen(fdesc)), tmp);
 
+		char* ext_desc = NULL;
 		if(nextline[0]==' ' || strlen(p)>LEN_FDESC) {	/* ext desc */
 			if(!(mode&NO_EXTEND)) {
 				memset(ext, 0, sizeof(ext));
-				f.misc|=FM_EXTDESC;
-				sprintf(ext,"%s\r\n",p);
+				sprintf(ext,"%s\r\n",p); 
+				ext_desc = ext;
 			}
 
 			if(nextline[0]==' ') {
 				SAFECOPY(str,nextline);				   /* tack on to end of desc */
 				p=str+dskip;
 				while(*p>0 && *p<=' ') p++;
-				i=LEN_FDESC-strlen(f.desc);
+				i=LEN_FDESC-strlen(fdesc);
 				if(i>1) {
 					p[i-1]=0;
 					truncsp(p);
@@ -509,8 +375,8 @@ void addlist(char *inpath, file_t f, uint dskip, uint sskip)
 				truncsp(nextline);
 				printf("%s\n",nextline);
 				if(!(mode&NO_EXTEND)) {
-					f.misc|=FM_EXTDESC;
 					p=nextline+dskip;
+					ext_desc = ext;
 					while(*p==' ') p++;
 					SAFECAT(ext,p);
 					SAFECAT(ext,"\r\n");
@@ -520,7 +386,6 @@ void addlist(char *inpath, file_t f, uint dskip, uint sskip)
 			}
 		}
 
-
 		l=flength(filepath);
 		if(l<0L) {
 			printf("%s not found.\n",filepath);
@@ -533,28 +398,31 @@ void addlist(char *inpath, file_t f, uint dskip, uint sskip)
 		if(sskip) l=atol(fname+sskip);
 
 		if(mode&FILE_ID)
-			get_file_diz(&f, filepath, ext);
+			get_file_diz(&f, ext, sizeof(ext));
 
-		f.cdt=l;
-		f.dateuled=time32(NULL);
-		f.altpath=cur_altpath;
-		prep_desc(f.desc);
+		prep_file_desc(fdesc, fdesc);
 		if(mode&ASCII_ONLY)
-			strip_exascii(f.desc, f.desc);
+			strip_exascii(fdesc, fdesc);
+		memset(&f, 0, sizeof(f));
+		uint32_t cdt = (uint32_t)l;
+		smb_hfield_bin(&f, SMB_COST, cdt);
+		smb_hfield_str(&f, SMB_FILENAME, fname);
+		smb_hfield_str(&f, SMB_FILEDESC, fdesc);
+		smb_hfield_str(&f, SENDER, uploader);
 		if(exist) {
-			putfiledat(&scfg,&f);
+			result = smb_updatemsg(&smb, &f);
 			if(!(mode&NO_NEWDATE))
-				update_uldate(&scfg, &f);
+				reupload(&smb, &f);
 		}
 		else
-			addfiledat(&scfg,&f);
-		if(f.misc&FM_EXTDESC) {
-			truncsp(ext);
-			putextdesc(&scfg,f.dir,f.datoffset,ext);
-		}
+			result = smb_addfile(&smb, &f, SMB_SELFPACK, ext_desc, filepath);
+		smb_freefilemem(&f);
+		if(result != SMB_SUCCESS)
+			fprintf(stderr, "!ERROR %d (%s) writing to %s\n"
+				, result, smb.last_error, smb.file);
 
 		if(mode&UL_STATS)
-			updatestats(l);
+			updatestats((ulong)l);
 		files++;
 	} while(!feof(stream) && !ferror(stream));
 	fclose(stream);
@@ -562,44 +430,21 @@ void addlist(char *inpath, file_t f, uint dskip, uint sskip)
 		printf("\nDeleting %s\n",listpath);
 		remove(listpath);
 	}
-
+	smb_close(&smb);
+	strListFree(&fname_list);
 }
 
 void synclist(char *inpath, int dirnum)
 {
 	char	str[1024];
-	char	fname[MAX_PATH+1];
 	char	listpath[MAX_PATH+1];
-	uchar*	ixbbuf;
 	char*	p;
-	int		i,file,found;
-	long	l,m,length;
+	int		found;
+	long	l;
 	FILE*	stream;
-	file_t	f;
-
-	sprintf(str,"%s%s.ixb",scfg.dir[dirnum]->data_dir,scfg.dir[dirnum]->code);
-	if((file=nopen(str,O_RDONLY|O_BINARY))==-1) {
-		printf("ERR_OPEN %s\n",str);
-		return;
-	}
-	length=filelength(file);
-	if(length%F_IXBSIZE) {
-		close(file);
-		printf("ERR_LEN (%ld) of %s\n",length,str);
-		return;
-	}
-	if((ixbbuf=(uchar *)malloc(length))==NULL) {
-		close(file);
-		printf("ERR_ALLOC %s\n",str);
-		return;
-	}
-	if(read(file,ixbbuf,length)!=length) {
-		close(file);
-		free((char *)ixbbuf);
-		printf("ERR_READ %s\n",str);
-		return;
-	}
-	close(file);
+	file_t*	f;
+	file_t*	file_list;
+	smb_t	smb;
 
 	SAFECOPY(listpath,inpath);
 	if((stream=fopen(listpath,"r"))==NULL) {
@@ -612,18 +457,19 @@ void synclist(char *inpath, int dirnum)
 		}
 	}
 
+	int result = smb_open_dir(&scfg, &smb, dirnum);
+	if(result != SMB_SUCCESS) {
+		fprintf(stderr, "!Error %d (%s) opening %s\n", result, smb.last_error, smb.file);
+		return;
+	}
+	size_t file_count;
+	file_list = loadfiles(&smb, NULL, 0, /* extdesc: */FALSE, FILE_SORT_NATURAL, &file_count);
+
 	printf("\nSynchronizing %s with %s %s\n\n"
 		,listpath,scfg.lib[scfg.dir[dirnum]->lib]->sname,scfg.dir[dirnum]->sname);
 
-	for(l=0;l<length;l+=F_IXBSIZE) {
-		m=l;
-		for(i=0;i<12 && l<length;i++)
-			if(i==8)
-				str[i]=ixbbuf[m]>' ' ? '.' : ' ';
-			else
-				str[i]=ixbbuf[m++]; 	/* Turns FILENAMEEXT into FILENAME.EXT */
-		str[i]=0;
-		unpadfname(str,fname);
+	for(l = 0; l < (long)file_count; l++) {
+		f = &file_list[l];
 		rewind(stream);
 		found=0;
 		while(!found) {
@@ -632,34 +478,28 @@ void synclist(char *inpath, int dirnum)
 			truncsp(str);
 			p=strchr(str,' ');
 			if(p) *p=0;
-			if(!stricmp(str,fname))
-				found=1;
+			if(!stricmp(str, f->name))
+				found=1; 
 		}
 		if(found)
 			continue;
-		padfname(fname,f.name);
-		printf("%s not found in list - ",f.name);
-		f.dir=dirnum;
-		f.datoffset=ixbbuf[m]|((long)ixbbuf[m+1]<<8)|((long)ixbbuf[m+2]<<16);
-		if(!getfiledat(&scfg,&f)) {
-			fprintf(stderr, "!ERROR reading index of directory %u\n", f.dir);
-			continue;
-		}
-		if(f.opencount) {
-			printf("currently OPEN by %u users\n",f.opencount);
-			continue;
+		printf("%s not found in list - ", f->name);
+		if(smb_removefile(&smb, f) != SMB_SUCCESS)
+			fprintf(stderr, "!ERROR (%s) removing file from database\n", smb.last_error);
+		else {
+			if(remove(getfilepath(&scfg, f, str)))
+				printf("Error removing %s\n",str);
+			removed++;
+			printf("Removed from database\n");
 		}
-		removefiledat(&scfg,&f);
-		if(remove(getfilepath(&scfg,&f,str)))
-			printf("Error removing %s\n",str);
-		removed++;
-		printf("Removed from database\n");
 	}
+	freefiles(file_list, file_count);
 
 	if(mode&DEL_LIST) {
 		printf("\nDeleting %s\n",listpath);
 		remove(listpath);
 	}
+	smb_close(&smb);
 	fclose(stream);
 }
 
@@ -698,24 +538,24 @@ char *usage="\nusage: addfiles code [.alt_path] [-opts] +list "
 int main(int argc, char **argv)
 {
 	char error[512];
-	char revision[16];
 	char str[MAX_PATH+1];
 	char tmp[MAX_PATH+1];
 	char *p;
-	char exist,listgiven=0,namegiven=0,ext[513]
+	char exist,listgiven=0,namegiven=0,ext[LEN_EXTDESC + 1]
 		,auto_name[MAX_PATH+1]="FILES.BBS";
 	int i,j;
 	uint desc_offset=0, size_offset=0;
 	long l;
+	smb_t	smb;
 	file_t	f;
+	uint dirnum = INVALID_DIR;
 
-	sscanf("$Revision: 1.63 $", "%*s %s", revision);
-
-	fprintf(stderr,"\nADDFILES v%s-%s (rev %s) - Adds Files to Synchronet "
+	fprintf(stderr,"\nADDFILES v%s-%s %s/%s - Adds Files to Synchronet "
 		"Filebase\n"
 		,ADDFILES_VER
 		,PLATFORM_DESC
-		,revision
+		,GIT_BRANCH
+		,GIT_HASH
 		);
 
 	if(argc<2) {
@@ -739,6 +579,7 @@ int main(int argc, char **argv)
 	}
 	SAFECOPY(scfg.temp_dir,"../temp");
 	prep_dir(scfg.ctrl_dir, scfg.temp_dir, sizeof(scfg.temp_dir));
+	memset(&smb, 0, sizeof(smb));
 
 	if(argv[1][0]=='*' || argv[1][0]=='-') {
 		if(argv[1][1]=='?') {
@@ -761,18 +602,18 @@ int main(int argc, char **argv)
 
 		if(i>=scfg.total_dirs) {
 			printf("Directory code '%s' not found.\n",argv[1]);
-			exit(1);
-		}
+			exit(1); 
+		} 
+		dirnum = i;
 	}
 
-	memset(&f,0,sizeof(file_t));
-	f.dir=i;
-	SAFECOPY(f.uler,"-> ADDFILES <-");
+	memset(&f,0,sizeof(f));
+	const char* uploader = "-> ADDFILES <-";
 
 	for(j=2;j<argc;j++) {
 		if(argv[j][0]=='*')     /* set the uploader name (legacy) */
-			SAFECOPY(f.uler,argv[j]+1);
-		else
+			uploader = argv[j]+1;
+		else 
 			if(argv[j][0]=='-'
 			|| argv[j][0]=='/'
 			) {      /* options */
@@ -811,7 +652,7 @@ int main(int argc, char **argv)
 							puts(usage);
 							return(-1);
 						}
-						SAFECOPY(f.uler,argv[j]);
+						uploader = argv[j];
 						i=strlen(argv[j])-1;
 						break;
 					case 'Z':
@@ -869,18 +710,18 @@ int main(int argc, char **argv)
 				&& IS_DIGIT(argv[j+1][0])) { /* skip x characters before description */
 				if(argc > j+2
 					&& IS_DIGIT(argv[j+2][0])) { /* skip x characters before size */
-					addlist(argv[j]+1,f,atoi(argv[j+1]),atoi(argv[j+2]));
+					addlist(argv[j]+1, dirnum, uploader, atoi(argv[j+1]), atoi(argv[j+2]));
 					j+=2;
 				}
 				else {
-					addlist(argv[j]+1,f,atoi(argv[j+1]),0);
-					j++;
-				}
+					addlist(argv[j]+1, dirnum, uploader, atoi(argv[j+1]), 0);
+					j++; 
+				} 
 			}
 			else
-				addlist(argv[j]+1,f,0,0);
+				addlist(argv[j]+1, dirnum, uploader, 0, 0);
 			if(mode&SYNC_LIST)
-				synclist(argv[j]+1,f.dir);
+				synclist(argv[j]+1, dirnum); 
 		}
 		else if(argv[j][0]=='.') {      /* alternate file path */
 			cur_altpath=atoi(argv[j]+1);
@@ -891,86 +732,69 @@ int main(int argc, char **argv)
 		}
 		else {
 			namegiven=1;
-			padfname(argv[j],f.name);
-			f.desc[0]=0;
-			memset(ext, 0, sizeof(ext));
-#if 0
-			strupr(f.name);
-#endif
+			const char* fname = argv[j];
+			char fdesc[LEN_FDESC + 1] = {0};
+
 			if(j+1==argc) {
-				printf("%s no description given.\n",f.name);
-				SAFECOPY(f.desc, "no description given");
+				printf("%s no description given.\n",fname);
+				SAFECOPY(fdesc, "no description given");
 			}
+
 			sprintf(str,"%s%s",cur_altpath ? scfg.altpath[cur_altpath-1]
-				: scfg.dir[f.dir]->path,argv[j]);
-			if(mode&TODAYS_DATE) {
-				time_t now = time(NULL);
-				if(datefmt) {
-					struct tm tm = {0};
-					localtime_r(&now, &tm);
-					strftime(f.desc, sizeof(f.desc), datefmt, &tm);
-				} else
-					SAFECOPY(f.desc, unixtodstr(&scfg, (time32_t)now, tmp));
-				SAFECAT(f.desc, "  ");
-			}
-			else if(mode&FILE_DATE) {
-				time_t file_timestamp = fdate(str);
-				if(datefmt) {
-					struct tm tm = {0};
-					localtime_r(&file_timestamp, &tm);
-					strftime(f.desc, sizeof(f.desc), datefmt, &tm);
-				} else
-					SAFECOPY(f.desc, unixtodstr(&scfg,(time32_t)file_timestamp,tmp));
-				SAFECAT(f.desc, "  ");
-			}
-			if(j+1 < argc) {
-				j++;
-				SAFECAT(f.desc, argv[j]);
-			}
-			l=flength(str);
+				: scfg.dir[dirnum]->path, fname);
+			if(mode&FILE_DATE)
+				sprintf(fdesc, "%s  ", unixtodstr(&scfg,(time32_t)fdate(str),tmp));
+			if(mode&TODAYS_DATE)
+				sprintf(fdesc, "%s  ", unixtodstr(&scfg,time32(NULL),tmp));
+			sprintf(tmp, "%.*s", (int)(LEN_FDESC-strlen(fdesc)), argv[++j]);
+			SAFECOPY(fdesc, tmp);
+			l=(long)flength(str);
 			if(l==-1) {
 				printf("%s not found.\n",str);
 				continue;
 			}
-			exist=findfile(&scfg,f.dir,f.name);
+			if(SMB_IS_OPEN(&smb))
+				smb_close(&smb);
+
+			int result = smb_open_dir(&scfg, &smb, dirnum);
+			if(result != SMB_SUCCESS) {
+				fprintf(stderr, "!ERROR %d (%s) opening %s\n", result, smb.last_error, smb.file);
+				exit(EXIT_FAILURE);
+			}
+
+			exist = smb_findfile(&smb, fname, &f) == SMB_SUCCESS;
 			if(exist) {
 				if(mode&NO_UPDATE)
 					continue;
-				if(!getfileixb(&scfg,&f)) {
-					fprintf(stderr, "!ERROR reading index of directory %u\n", f.dir);
-					continue;
-				}
-				if((mode&CHECK_DATE) && fdate(str) <= f.dateuled)
+				if((mode&CHECK_DATE) && fdate(str) <= f.idx.time)
 					continue;
 				if(mode&ULDATE_ONLY) {
-					f.dateuled=time32(NULL);
-					update_uldate(&scfg, &f);
-					continue;
-				}
-				if (f.misc & FM_EXTDESC)
-					getextdesc(&scfg, f.dir, f.datoffset, ext);
+					reupload(&smb, &f);
+					continue; 
+				} 
 			}
-			f.cdt=l;
-			f.dateuled=time32(NULL);
-			f.altpath=cur_altpath;
-			prep_desc(f.desc);
+			memset(&f, 0, sizeof(f));
+			prep_file_desc(fdesc, fdesc);
 			if(mode&ASCII_ONLY)
-				strip_exascii(f.desc, f.desc);
-			printf("%s %7"PRIu32" %s\n",f.name,f.cdt,f.desc);
-			if(mode&FILE_ID)
-				get_file_diz(&f, str, ext);
-
+				strip_exascii(fdesc, fdesc);
+			smb_hfield_str(&f, SMB_FILENAME, fname);
+			smb_hfield_str(&f, SMB_FILEDESC, fdesc);
+			smb_hfield_str(&f, SENDER, uploader);
+			uint32_t cdt = (uint32_t)l;
+			smb_hfield_bin(&f, SMB_COST, cdt);
+
+			printf("%s %7"PRIu64" %s\n",fname, (int64_t)l, fdesc);
+			char* ext_desc = NULL;
+			if(get_file_diz(&f, ext, sizeof(ext)))
+				ext_desc = ext;
 			if(exist) {
-				putfiledat(&scfg,&f);
 				if(!(mode&NO_NEWDATE))
-					update_uldate(&scfg, &f);
+					reupload(&smb, &f);
+				else
+					result = smb_updatemsg(&smb, &f);
 			}
 			else
-				addfiledat(&scfg,&f);
-
-			if(f.misc&FM_EXTDESC)
-				putextdesc(&scfg,f.dir,f.datoffset,ext);
-
+				result = smb_addfile(&smb, &f, SMB_SELFPACK, ext_desc, str);
 			if(mode&UL_STATS)
 				updatestats(l);
 			files++;
@@ -983,23 +807,23 @@ int main(int argc, char **argv)
 				continue;
 			if(scfg.dir[i]->misc&DIR_NOAUTO)
 				continue;
-			f.dir=i;
+			dirnum = i;
 			if(mode&SEARCH_DIR) {
-				addlist("",f,desc_offset,size_offset);
-				continue;
+				addlist("", dirnum, uploader, desc_offset, size_offset);
+				continue; 
 			}
-			sprintf(str,"%s%s.lst",scfg.dir[f.dir]->path,scfg.dir[f.dir]->code);
+			sprintf(str,"%s%s.lst",scfg.dir[dirnum]->path,scfg.dir[dirnum]->code);
 			if(fexistcase(str) && flength(str)>0L) {
 				printf("Auto-adding %s\n",str);
-				addlist(str,f,desc_offset,size_offset);
+				addlist(str, dirnum, uploader, desc_offset, size_offset);
 				if(mode&SYNC_LIST)
 					synclist(str,i);
 				continue;
 			}
-			SAFEPRINTF2(str,"%s%s",scfg.dir[f.dir]->path,auto_name);
+			SAFEPRINTF2(str,"%s%s",scfg.dir[dirnum]->path,auto_name);
 			if(fexistcase(str) && flength(str)>0L) {
 				printf("Auto-adding %s\n",str);
-				addlist(str,f,desc_offset,size_offset);
+				addlist(str, dirnum, uploader, desc_offset, size_offset);
 				if(mode&SYNC_LIST)
 					synclist(str,i);
 				continue;
@@ -1009,12 +833,11 @@ int main(int argc, char **argv)
 
 	else {
 		if(!listgiven && !namegiven) {
-			sprintf(str,"%s%s.lst",scfg.dir[f.dir]->path, scfg.dir[f.dir]->code);
+			sprintf(str,"%s%s.lst",scfg.dir[dirnum]->path, scfg.dir[dirnum]->code);
 			if(!fexistcase(str) || flength(str)<=0L)
-				SAFEPRINTF2(str,"%s%s",scfg.dir[f.dir]->path, auto_name);
-			addlist(str,f,desc_offset,size_offset);
+				SAFEPRINTF2(str,"%s%s",scfg.dir[dirnum]->path, auto_name);
 			if(mode&SYNC_LIST)
-				synclist(str,f.dir);
+				synclist(str, dirnum);
 		}
 	}
 
diff --git a/src/sbbs3/addfiles.vcxproj b/src/sbbs3/addfiles.vcxproj
index 941a6ebf5076bcf1fe26dcfb16f74b85f8a0209a..8307ed9c89293c47f76996f588f6d0b305bbb0d6 100644
--- a/src/sbbs3/addfiles.vcxproj
+++ b/src/sbbs3/addfiles.vcxproj
@@ -38,6 +38,7 @@
     <Import Project="..\build\undeprecate.props" />
     <Import Project="..\build\target_ia32.props" />
     <Import Project="..\hash\hash.props" />
+    <Import Project="..\..\3rdp\win32.release\libarchive\libarchive.props" />
   </ImportGroup>
   <ImportGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="PropertySheets">
     <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
@@ -47,6 +48,7 @@
     <Import Project="..\build\undeprecate.props" />
     <Import Project="..\build\target_ia32.props" />
     <Import Project="..\hash\hash.props" />
+    <Import Project="..\..\3rdp\win32.release\libarchive\libarchive.props" />
   </ImportGroup>
   <PropertyGroup Label="UserMacros" />
   <PropertyGroup>
@@ -96,6 +98,7 @@
       <TargetMachine>MachineX86</TargetMachine>
       <IgnoreSpecificDefaultLibraries>libcd.lib;%(IgnoreSpecificDefaultLibraries)</IgnoreSpecificDefaultLibraries>
       <ImageHasSafeExceptionHandlers>false</ImageHasSafeExceptionHandlers>
+      <AdditionalDependencies>netapi32.lib;wsock32.lib;%(AdditionalDependencies)</AdditionalDependencies>
     </Link>
     <Bscmake>
       <SuppressStartupBanner>true</SuppressStartupBanner>
@@ -139,6 +142,7 @@
       <TargetMachine>MachineX86</TargetMachine>
       <IgnoreSpecificDefaultLibraries>libcd.lib;%(IgnoreSpecificDefaultLibraries)</IgnoreSpecificDefaultLibraries>
       <ImageHasSafeExceptionHandlers>false</ImageHasSafeExceptionHandlers>
+      <AdditionalDependencies>netapi32.lib;wsock32.lib;%(AdditionalDependencies)</AdditionalDependencies>
     </Link>
     <Bscmake>
       <SuppressStartupBanner>true</SuppressStartupBanner>
@@ -183,7 +187,6 @@
     </ProjectReference>
     <ProjectReference Include="..\xpdev\xpdev.vcxproj">
       <Project>{7428a1e8-56b7-4868-9c0e-29d031689feb}</Project>
-      <ReferenceOutputAssembly>false</ReferenceOutputAssembly>
     </ProjectReference>
     <ProjectReference Include="load_cfg.vcxproj">
       <Project>{08fc395f-bc60-499d-9ce9-170ed718bb94}</Project>
diff --git a/src/sbbs3/allusers.c b/src/sbbs3/allusers.c
index 7ac0bdec2e8cbdccf02c48368a8188827770e7f2..047741ae26920cdb7ad6dc02671408f2e581fd5b 100644
--- a/src/sbbs3/allusers.c
+++ b/src/sbbs3/allusers.c
@@ -146,7 +146,8 @@ int main(int argc, char **argv)
 {
 	char	dir[128],str[128];
 	int 	i,j,file,set,sub,mod;
-	long	l,f,flags,flagoff,length,offset;
+	long	l,f,flags,flagoff,offset;
+	off_t	length;
 	FILE	*stream;
 
 	printf("\nALLUSERS v2.10 - Bulk User Editor for Synchronet User Database\n");
@@ -220,12 +221,12 @@ int main(int argc, char **argv)
 						exit(1); 
 					}
 					setvbuf(stream,NULL,_IOFBF,2048);
-					length=filelength(file);
+					length=(ulong)filelength(file);
 					printf("\n%s Flags %s Set #%d\n",sub ? "Removing":"Adding"
 						,sub ? "from":"to",set);
 					for(offset=0;offset<length;offset+=U_LEN) {
 						printf("%lu of %lu (%u modified)\r"
-							,(offset/U_LEN)+1,length/U_LEN,mod);
+							,(offset/U_LEN)+1,(ulong)(length/U_LEN),mod);
 						if(lockuser(stream,offset)) {
 							printf("Error locking offset %lu\n",offset);
 							continue; 
@@ -290,7 +291,7 @@ int main(int argc, char **argv)
 						,flagoff==U_REST ? "Restrictions":"Exemptions");
 					for(offset=0;offset<length;offset+=U_LEN) {
 						printf("%lu of %lu (%u modified)\r"
-							,(offset/U_LEN)+1,length/U_LEN,mod);
+							,(offset/U_LEN)+1,(ulong)(length/U_LEN),mod);
 						if(lockuser(stream,offset)) {
 							printf("Error locking offset %lu\n",offset);
 							continue; 
@@ -343,7 +344,7 @@ int main(int argc, char **argv)
 					printf("\nChanging Levels\n");
 					for(offset=0;offset<length;offset+=U_LEN) {
 						printf("%lu of %lu (%u modified)\r"
-							,(offset/U_LEN)+1,length/U_LEN,mod);
+							,(offset/U_LEN)+1,(ulong)(length/U_LEN),mod);
 						if(lockuser(stream,offset)) {
 							printf("Error locking offset %lu\n",offset);
 							continue; 
diff --git a/src/sbbs3/atcodes.cpp b/src/sbbs3/atcodes.cpp
index 28f8c44829d1ee56e9b96d219dc26ced494873e4..f258c80f879d757382be779f7311c391e8fcb249 100644
--- a/src/sbbs3/atcodes.cpp
+++ b/src/sbbs3/atcodes.cpp
@@ -1990,31 +1990,33 @@ const char* sbbs_t::atcode(char* sp, char* str, size_t maxlen, long* pmode, bool
 			return current_file->name;
 		if(strcmp(sp, "FILE_DESC") == 0)
 			return current_file->desc;
+		if(strcmp(sp, "FILE_TAGS") == 0)
+			return current_file->tags;
 		if(strcmp(sp, "FILE_UPLOADER") == 0)
-			return current_file->uler;
+			return current_file->from;
 		if(strcmp(sp, "FILE_SIZE") == 0) {
 			safe_snprintf(str, maxlen, "%ld", (long)current_file->size);
 			return str;
 		}
 		if(strcmp(sp, "FILE_CREDITS") == 0) {
-			safe_snprintf(str, maxlen, "%lu", (ulong)current_file->cdt);
+			safe_snprintf(str, maxlen, "%lu", (ulong)current_file->cost);
 			return str;
 		}
 		if(strcmp(sp, "FILE_TIME") == 0)
-			return timestr(current_file->date);
+			return timestr(current_file->time);
 		if(strcmp(sp, "FILE_TIME_ULED") == 0)
-			return timestr(current_file->dateuled);
+			return timestr(current_file->hdr.when_imported.time);
 		if(strcmp(sp, "FILE_TIME_DLED") == 0)
-			return timestr(current_file->datedled);
+			return timestr(current_file->hdr.last_downloaded);
 		if(strcmp(sp, "FILE_DATE") == 0)
-			return datestr(current_file->date);
+			return datestr(current_file->time);
 		if(strcmp(sp, "FILE_DATE_ULED") == 0)
-			return datestr(current_file->dateuled);
+			return datestr(current_file->hdr.when_imported.time);
 		if(strcmp(sp, "FILE_DATE_DLED") == 0)
-			return datestr(current_file->datedled);
+			return datestr(current_file->hdr.last_downloaded);
 
 		if(strcmp(sp, "FILE_TIMES_DLED") == 0) {
-			safe_snprintf(str, maxlen, "%d", current_file->timesdled);
+			safe_snprintf(str, maxlen, "%lu", (ulong)current_file->hdr.times_downloaded);
 			return str;
 		}
 	}
diff --git a/src/sbbs3/bat_xfer.cpp b/src/sbbs3/bat_xfer.cpp
index 7c75c2779c288384268decd3e4806add93f42f7d..518f87b3e7d6f317c2887454aadfacf56c457f47 100644
--- a/src/sbbs3/bat_xfer.cpp
+++ b/src/sbbs3/bat_xfer.cpp
@@ -1,9 +1,5 @@
-/* bat_xfer.cpp */
-
 /* Synchronet batch file transfer functions */
 
-/* $Id: bat_xfer.cpp,v 1.41 2020/05/13 23:56:08 rswindell Exp $ */
-
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
  * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
@@ -17,25 +13,14 @@
  * See the GNU General Public License for more details: gpl.txt or			*
  * http://www.fsf.org/copyleft/gpl.html										*
  *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
- *																			*
  * For Synchronet coding style and modification guidelines, see				*
  * http://www.synchro.net/source.html										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
 #include "sbbs.h"
+#include "filedat.h"
 
 /****************************************************************************/
 /* This is the batch menu section                                           */
@@ -46,19 +31,19 @@ void sbbs_t::batchmenu()
 	char 	tmp[512];
 	char	keys[32];
 	uint	i,n,xfrprot,xfrdir;
-    ulong	totalcdt,totalsize,totaltime;
+    int64_t	totalcdt,totalsize;
     time_t	start,end;
-    file_t	f;
+	str_list_t ini;
+	str_list_t filenames;
 
-	if(!batdn_total && !batup_total && cfg.upload_dir==INVALID_DIR) {
+	if(batdn_total() < 1 && batup_total() < 1 && cfg.upload_dir==INVALID_DIR) {
 		bputs(text[NoFilesInBatchQueue]);
 		return; 
 	}
 	if(useron.misc&(RIP|WIP|HTML) && !(useron.misc&EXPERT))
 		menu("batchxfer");
 	lncntr=0;
-	while(online && !done && (batdn_total || batup_total
-		|| cfg.upload_dir!=INVALID_DIR)) {
+	while(online && !done && (cfg.upload_dir!=INVALID_DIR || batdn_total() || batup_total())) {
 		if(!(useron.misc&(EXPERT|RIP|WIP|HTML))) {
 			sys_status&=~SS_ABORT;
 			if(lncntr) {
@@ -71,7 +56,7 @@ void sbbs_t::batchmenu()
 		}
 		ASYNC;
 		bputs(text[BatchMenuPrompt]);
-		SAFEPRINTF(keys,"BCDLRU?\r%c", text[YNQP][2]);
+		SAFEPRINTF(keys,"CDLRU?\r%c", text[YNQP][2]);
 		ch=(char)getkeys(keys,0);
 		if(ch>' ')
 			logch(ch,0);
@@ -85,125 +70,21 @@ void sbbs_t::batchmenu()
 				if(useron.misc&(EXPERT|RIP|WIP|HTML))
 					menu("batchxfr");
 				break;
-			case 'B':   /* Bi-directional transfers */
-				if(useron.rest&FLAG('D')) {
-					bputs(text[R_Download]);
-					break; 
-				}
-				if(useron.rest&FLAG('U')) {
-					bputs(text[R_Upload]);
-					break; 
-				}
-				if(!batdn_total) {
-					bputs(text[DownloadQueueIsEmpty]);
-					break; 
-				}
-				if(!batup_total && cfg.upload_dir==INVALID_DIR) {
-					bputs(text[UploadQueueIsEmpty]);
-					break; 
-				}
-				for(i=0,totalcdt=0;i<batdn_total;i++)
-					if(!is_download_free(&cfg,batdn_dir[i],&useron,&client))
-						totalcdt+=batdn_cdt[i];
-				if(totalcdt>useron.cdt+useron.freecdt) {
-					bprintf(text[YouOnlyHaveNCredits]
-						,ultoac(useron.cdt+useron.freecdt,tmp));
-					break; 
-				}
-				for(i=0,totalsize=totaltime=0;i<batdn_total;i++) {
-					totalsize+=batdn_size[i];
-					if(!(cfg.dir[batdn_dir[i]]->misc&DIR_TFREE) && cur_cps)
-						totaltime+=batdn_size[i]/(ulong)cur_cps; 
-				}
-				if(!(useron.exempt&FLAG('T')) && !SYSOP && totaltime>timeleft) {
-					bputs(text[NotEnoughTimeToDl]);
-					break; 
-				}
-				xfer_prot_menu(XFER_BIDIR);
-				SYNC;
-				mnemonics(text[ProtocolOrQuit]);
-				SAFEPRINTF(tmp2,"%c",text[YNQP][2]);
-				for(i=0;i<cfg.total_prots;i++)
-					if(cfg.prot[i]->bicmd[0] && chk_ar(cfg.prot[i]->ar,&useron,&client)) {
-						sprintf(tmp,"%c",cfg.prot[i]->mnemonic);
-						SAFECAT(tmp2,tmp); 
-					}
-				ungetkey(useron.prot);
-				ch=(char)getkeys(tmp2,0);
-				if(ch==text[YNQP][2])
-					break;
-				for(i=0;i<cfg.total_prots;i++)
-					if(cfg.prot[i]->bicmd[0] && cfg.prot[i]->mnemonic==ch
-						&& chk_ar(cfg.prot[i]->ar,&useron,&client))
-						break;
-				if(i<cfg.total_prots) {
-					if(!create_batchdn_lst((cfg.prot[i]->misc&PROT_NATIVE) ? true:false))
-						break;
-					if(!create_batchup_lst())
-						break;
-					if(!create_bimodem_pth())
-						break;
-
-					xfrprot=i;
-					action=NODE_BXFR;
-					SYNC;
-					for(i=0;i<batdn_total;i++)
-						if(cfg.dir[batdn_dir[i]]->seqdev) {
-							lncntr=0;
-							unpadfname(batdn_name[i],tmp);
-							SAFEPRINTF2(tmp2,"%s%s",cfg.temp_dir,tmp);
-							if(!fexistcase(tmp2)) {
-								seqwait(cfg.dir[batdn_dir[i]]->seqdev);
-								bprintf(text[RetrievingFile],tmp);
-								SAFEPRINTF2(str,"%s%s"
-									,batdn_alt[i]>0 && batdn_alt[i]<=cfg.altpaths
-									? cfg.altpath[batdn_alt[i]-1]
-									: cfg.dir[batdn_dir[i]]->path
-									,tmp);
-								mv(str,tmp2,1); /* copy the file to temp dir */
-								if(getnodedat(cfg.node_num,&thisnode,true)==0) {
-									thisnode.aux=0xff;
-									putnodedat(cfg.node_num,&thisnode);
-								}
-								CRLF; 
-							} 
-						}
-					SAFEPRINTF(str,"%sBATCHDN.LST",cfg.node_dir);
-					SAFEPRINTF(tmp2,"%sBATCHUP.LST",cfg.node_dir);
-					start=time(NULL);
-					protocol(cfg.prot[xfrprot],XFER_BIDIR,str,tmp2,true);
-					end=time(NULL);
-					for(i=0;i<batdn_total;i++)
-						if(cfg.dir[batdn_dir[i]]->seqdev) {
-							unpadfname(batdn_name[i],tmp);
-							SAFEPRINTF2(tmp2,"%s%s",cfg.temp_dir,tmp);
-							remove(tmp2); 
-						}
-					batch_upload();
-					batch_download(xfrprot);
-					if(batdn_total)     /* files still in queue, not xfered */
-						notdownloaded(totalsize,start,end);
-					autohangup(); 
-				}
-				break;
 			case 'C':
-				if(batup_total) {
+				if(batup_total() < 1) {
+					bputs(text[UploadQueueIsEmpty]);
+				} else {
 					if(text[ClearUploadQueueQ][0]==0 || !noyes(text[ClearUploadQueueQ])) {
-						batup_total=0;
-						bputs(text[UploadQueueCleared]); 
+						if(clearbatul())
+							bputs(text[UploadQueueCleared]);
 					} 
 				}
-				if(batdn_total) {
+				if(batdn_total() <1 ) {
+					bputs(text[DownloadQueueIsEmpty]);
+				} else {
 					if(text[ClearDownloadQueueQ][0]==0 || !noyes(text[ClearDownloadQueueQ])) {
-						for(i=0;i<batdn_total;i++) {
-							f.dir=batdn_dir[i];
-							f.datoffset=batdn_offset[i];
-							f.size=batdn_size[i];
-							SAFECOPY(f.name,batdn_name[i]);
-							closefile(&f); 
-						}
-						batdn_total=0;
-						bputs(text[DownloadQueueCleared]); 
+						if(clearbatdl())
+							bputs(text[DownloadQueueCleared]); 
 					} 
 				}
 				break;
@@ -211,72 +92,68 @@ void sbbs_t::batchmenu()
 				start_batch_download();
 				break;
 			case 'L':
-				if(batup_total) {
+				ini = batch_list_read(&cfg, useron.number, XFER_BATCH_UPLOAD);
+				filenames = iniGetSectionList(ini, NULL);
+				if(strListCount(filenames)) {
 					bputs(text[UploadQueueLstHdr]);
-					for(i=0;i<batup_total;i++)
-						bprintf(text[UploadQueueLstFmt],i+1,batup_name[i]
-							,batup_desc[i]); 
-				}
-				if(batdn_total) {
+					for(size_t i = 0; filenames[i]; ++i) {
+						const char* filename = filenames[i];
+						char value[INI_MAX_VALUE_LEN];
+						bprintf(text[UploadQueueLstFmt]
+							,i+1, filename
+							,iniGetString(ini, filename, "desc", text[NoDescription], value));
+					}
+				} else
+					bputs(text[UploadQueueIsEmpty]);
+
+				iniFreeStringList(filenames);
+				iniFreeStringList(ini);
+
+				totalsize = 0;
+				totalcdt = 0;
+				ini = batch_list_read(&cfg, useron.number, XFER_BATCH_DOWNLOAD);
+				filenames = iniGetSectionList(ini, NULL);
+				if(strListCount(filenames)) {
 					bputs(text[DownloadQueueLstHdr]);
-					for(i=0,totalcdt=0,totalsize=0;i<batdn_total;i++) {
+					for(size_t i = 0; filenames[i]; ++i) {
+						const char* filename = filenames[i];
+						file_t f = {{}};
+						if(!batch_file_load(&cfg, ini, filename, &f))
+							continue;
+						getfilesize(&cfg, &f);
+						getfiletime(&cfg, &f);
 						bprintf(text[DownloadQueueLstFmt],i+1
-							,batdn_name[i],ultoac(batdn_cdt[i],tmp)
-							,ultoac(batdn_size[i],str)
+							,filename
+							,ultoac(f.cost, tmp)
+							,ultoac((ulong)f.size, str)
 							,cur_cps
-							? sectostr(batdn_size[i]/(ulong)cur_cps,tmp2)
-							: "??:??:??");
-						totalsize+=batdn_size[i];
-						totalcdt+=batdn_cdt[i]; 
+								? sectostr((uint)(f.size/(ulong)cur_cps),tmp2)
+								: "??:??:??"
+							,datestr(f.time)
+						);
+						totalsize += f.size;
+						totalcdt += f.cost;
+						smb_freemsgmem(&f);
 					}
 					bprintf(text[DownloadQueueTotals]
-						,ultoac(totalcdt,tmp),ultoac(totalsize,str),cur_cps
-						? sectostr(totalsize/(ulong)cur_cps,tmp2)
+						,ultoac((ulong)totalcdt,tmp),ultoac((ulong)totalsize,str),cur_cps
+						? sectostr((ulong)totalsize/(ulong)cur_cps,tmp2)
 						: "??:??:??"); 
-				}
-				break;  /* Questionable line ^^^, see note above function */
+				} else
+					bputs(text[DownloadQueueIsEmpty]);
+				iniFreeStringList(filenames);
+				iniFreeStringList(ini);
+				break;
 			case 'R':
-				if(batup_total) {
-					bprintf(text[RemoveWhichFromUlQueue],batup_total);
-					n=getnum(batup_total);
-					if((int)n>=1) {
-						n--;
-						batup_total--;
-						while(n<batup_total) {
-							batup_dir[n]=batup_dir[n+1];
-							batup_misc[n]=batup_misc[n+1];
-							batup_alt[n]=batup_alt[n+1];
-							strcpy(batup_name[n],batup_name[n+1]);
-							strcpy(batup_desc[n],batup_desc[n+1]);
-							n++; 
-						}
-						if(!batup_total)
-							bputs(text[UploadQueueCleared]); 
-					} 
+				if((n = batup_total()) > 0) {
+					bprintf(text[RemoveWhichFromUlQueue], n);
+					if(getstr(str, MAX_FILENAME_LEN, K_NONE) > 0)
+						batch_file_remove(&cfg, useron.number, XFER_BATCH_UPLOAD, str);
 				}
-				if(batdn_total) {
-					bprintf(text[RemoveWhichFromDlQueue],batdn_total);
-					n=getnum(batdn_total);
-					if((int)n>=1) {
-						n--;
-						f.dir=batdn_dir[n];
-						SAFECOPY(f.name,batdn_name[n]);
-						f.datoffset=batdn_offset[n];
-						f.size=batdn_size[n];
-						closefile(&f);
-						batdn_total--;
-						while(n<batdn_total) {
-							strcpy(batdn_name[n],batdn_name[n+1]);
-							batdn_dir[n]=batdn_dir[n+1];
-							batdn_cdt[n]=batdn_cdt[n+1];
-							batdn_alt[n]=batdn_alt[n+1];
-							batdn_size[n]=batdn_size[n+1];
-							batdn_offset[n]=batdn_offset[n+1];
-							n++; 
-						}
-						if(!batdn_total)
-							bputs(text[DownloadQueueCleared]); 
-					} 
+				if((n = batdn_total()) > 0) {
+					bprintf(text[RemoveWhichFromDlQueue], n);
+					if(getstr(str, MAX_FILENAME_LEN, K_NONE) > 0)
+						batch_file_remove(&cfg, useron.number, XFER_BATCH_DOWNLOAD, str);
 				}
 				break;
 		   case 'U':
@@ -284,15 +161,13 @@ void sbbs_t::batchmenu()
 					bputs(text[R_Upload]);
 					break; 
 				}
-				if(!batup_total && cfg.upload_dir==INVALID_DIR) {
+				if(batup_total() < 1 && cfg.upload_dir==INVALID_DIR) {
 					bputs(text[UploadQueueIsEmpty]);
 					break; 
 				}
 				xfer_prot_menu(XFER_BATCH_UPLOAD);
 				if(!create_batchup_lst())
 					break;
-				if(!create_bimodem_pth())
-					break;
 				ASYNC;
 				mnemonics(text[ProtocolOrQuit]);
 				SAFEPRINTF(str,"%c",text[YNQP][2]);
@@ -311,10 +186,7 @@ void sbbs_t::batchmenu()
 				if(i<cfg.total_prots) {
 					SAFEPRINTF(str,"%sBATCHUP.LST",cfg.node_dir);
 					xfrprot=i;
-					if(batup_total)
-						xfrdir=batup_dir[0];
-					else
-						xfrdir=cfg.upload_dir;
+					xfrdir=cfg.upload_dir;
 					action=NODE_ULNG;
 					SYNC;
 					if(online==ON_REMOTE) {
@@ -342,41 +214,75 @@ BOOL sbbs_t::start_batch_download()
 {
 	char	ch;
 	char	tmp[32];
-	char	fname[64];
 	char	str[MAX_PATH+1];
 	char 	path[MAX_PATH+1];
-	char*	list;
-	size_t	list_len;
+	char	list[1024] = "";
 	int		error;
-    int		j;
     uint	i,xfrprot;
-    ulong	totalcdt,totalsize,totaltime;
     time_t	start,end,t;
 	struct	tm tm;
 
-	if(batdn_total==0) {
-		bputs(text[DownloadQueueIsEmpty]);
-		return(FALSE);
-	}
 	if(useron.rest&FLAG('D')) {     /* Download restriction */
 		bputs(text[R_Download]);
 		return(FALSE); 
 	}
-	for(i=0,totalcdt=0;i<batdn_total;i++)
-		if(is_download_free(&cfg,batdn_dir[i],&useron,&client))
-			totalcdt+=batdn_cdt[i];
-	if(totalcdt>useron.cdt+useron.freecdt) {
+
+	str_list_t ini = batch_list_read(&cfg, useron.number, XFER_BATCH_DOWNLOAD);
+
+	size_t file_count = iniGetSectionCount(ini, NULL);
+	if(file_count < 1) {
+		bputs(text[DownloadQueueIsEmpty]);
+		iniFreeStringList(ini);
+		return(FALSE);
+	}
+	str_list_t filenames = iniGetSectionList(ini, NULL);
+
+	if(file_count == 1) {	// Only one file in the queue? Perform a non-batch (e.g. XMODEM) download
+		file_t f = {{}};
+		BOOL result = FALSE;
+		if(batch_file_get(&cfg, ini, filenames[0], &f)) {
+			result = sendfile(&f, /* prot: */' ', /* autohang: */true);
+			if(result == TRUE)
+				batch_file_remove(&cfg, useron.number, XFER_BATCH_DOWNLOAD, f.name);
+		}
+		iniFreeStringList(ini);
+		iniFreeStringList(filenames);
+		smb_freefilemem(&f);
+		return result;
+	}
+
+	int64_t totalcdt = 0;
+	for(size_t i=0; filenames[i] != NULL; ++i) {
+		file_t f = {{}};
+		if(batch_file_load(&cfg, ini, filenames[i], &f)) {
+			totalcdt += f.cost;
+			smb_freefilemem(&f);
+		}
+	}
+	if(totalcdt > (int64_t)(useron.cdt+useron.freecdt)) {
 		bprintf(text[YouOnlyHaveNCredits]
 			,ultoac(useron.cdt+useron.freecdt,tmp));
+		iniFreeStringList(ini);
+		iniFreeStringList(filenames);
 		return(FALSE); 
 	}
 
-	for(i=0,totalsize=totaltime=0;i<batdn_total;i++) {
-		totalsize+=batdn_size[i];
-		if(!(cfg.dir[batdn_dir[i]]->misc&DIR_TFREE) && cur_cps)
-			totaltime+=batdn_size[i]/(ulong)cur_cps; 
+	int64_t totalsize = 0;
+	int64_t totaltime = 0;
+	for(size_t i=0; filenames[i] != NULL; ++i) {
+		file_t f = {{}};
+		if(!batch_file_get(&cfg, ini, filenames[i], &f))
+			continue;
+		if(!(cfg.dir[f.dir]->misc&DIR_TFREE) && cur_cps)
+			totaltime += getfilesize(&cfg, &f) / (ulong)cur_cps;
+		SAFECAT(list, getfilepath(&cfg, &f, path));
+		SAFECAT(list, " ");
+		smb_freefilemem(&f);
 	}
-	if(!(useron.exempt&FLAG('T')) && !SYSOP && totaltime>timeleft) {
+	iniFreeStringList(ini);
+	iniFreeStringList(filenames);
+
+	if(!(useron.exempt&FLAG('T')) && !SYSOP && totaltime > (int64_t)timeleft) {
 		bputs(text[NotEnoughTimeToDl]);
 		return(FALSE); 
 	}
@@ -397,15 +303,11 @@ BOOL sbbs_t::start_batch_download()
 		if(cfg.prot[i]->batdlcmd[0] && cfg.prot[i]->mnemonic==ch
 			&& chk_ar(cfg.prot[i]->ar,&useron,&client))
 			break;
-	if(i>=cfg.total_prots)
-		return(FALSE);	/* no protocol selected */
-
-	if(!create_batchdn_lst((cfg.prot[i]->misc&PROT_NATIVE) ? true:false))
-		return(FALSE);
-	if(!create_bimodem_pth())
+	if(i>=cfg.total_prots || !create_batchdn_lst((cfg.prot[i]->misc&PROT_NATIVE) ? true:false)) {
 		return(FALSE);
-
+	}
 	xfrprot=i;
+#if 0 // NFB-TODO: Download events
 	list=NULL;
 	for(i=0;i<batdn_total;i++) {
 		curdirnum=batdn_dir[i]; 		/* for ARS */
@@ -417,9 +319,7 @@ BOOL sbbs_t::start_batch_download()
 				seqwait(cfg.dir[batdn_dir[i]]->seqdev);
 				bprintf(text[RetrievingFile],fname);
 				SAFEPRINTF2(str,"%s%s"
-					,batdn_alt[i]>0 && batdn_alt[i]<=cfg.altpaths
-					? cfg.altpath[batdn_alt[i]-1]
-					: cfg.dir[batdn_dir[i]]->path
+					,cfg.dir[batdn_dir[i]]->path
 					,fname);
 				mv(str,path,1); /* copy the file to temp dir */
 				if(getnodedat(cfg.node_num,&thisnode,true)==0) {
@@ -462,6 +362,7 @@ BOOL sbbs_t::start_batch_download()
 			CRLF; 
 		}
 	}
+#endif
 
 	SAFEPRINTF(str,"%sBATCHDN.LST",cfg.node_dir);
 	action=NODE_DLNG;
@@ -479,13 +380,11 @@ BOOL sbbs_t::start_batch_download()
 	end=time(NULL);
 	if(cfg.prot[xfrprot]->misc&PROT_DSZLOG || !error)
 		batch_download(xfrprot);
-	if(batdn_total)
-		notdownloaded(totalsize,start,end);
-	autohangup(); 
-	if(list!=NULL)
-		free(list);
+	if(batdn_total())
+		notdownloaded((ulong)totalsize,start,end);
+	autohangup();
 
-	return(TRUE);
+	return TRUE;
 }
 
 /****************************************************************************/
@@ -495,31 +394,45 @@ BOOL sbbs_t::start_batch_download()
 bool sbbs_t::create_batchdn_lst(bool native)
 {
 	char	path[MAX_PATH+1];
-	char	fname[MAX_PATH+1];
-	int		file;
-	uint	i;
 
-	SAFEPRINTF(path,"%sBATCHDN.LST",cfg.node_dir);
-	if((file=nopen(path,O_WRONLY|O_CREAT|O_TRUNC))==-1) {
-		errormsg(WHERE,ERR_OPEN,path,O_WRONLY|O_CREAT|O_TRUNC);
-		return(false); 
+	SAFEPRINTF(path, "%sBATCHDN.LST", cfg.node_dir);
+	FILE* fp = fopen(path, "wb");
+	if(fp == NULL) {
+		errormsg(WHERE, ERR_OPEN, path);
+		return false;
 	}
-	for(i=0;i<batdn_total;i++) {
-		if(batdn_dir[i]>=cfg.total_dirs || cfg.dir[batdn_dir[i]]->seqdev)
-			SAFECOPY(path,cfg.temp_dir);
-		else
-			SAFECOPY(path,batdn_alt[i]>0 && batdn_alt[i]<=cfg.altpaths
-				? cfg.altpath[batdn_alt[i]-1] : cfg.dir[batdn_dir[i]]->path);
-
-		unpadfname(batdn_name[i],fname);
-		SAFECAT(path,fname);
-		if(native)
-			fexistcase(path);
-		SAFECAT(path,crlf);
-		write(file,path,strlen(path)); 
+	str_list_t ini = batch_list_read(&cfg, useron.number, XFER_BATCH_DOWNLOAD);
+	str_list_t filenames = iniGetSectionList(ini, /* prefix: */NULL);
+	for(size_t i = 0; filenames[i] != NULL; ++i) {
+		const char* filename = filenames[i];
+		file_t f = {};
+		f.dir = batch_file_dir(&cfg, ini, filename);
+		if(!loadfile(&cfg, f.dir, filename, &f, file_detail_index)) {
+			errormsg(WHERE, "loading file", filename, i);
+			batch_file_remove(&cfg, useron.number, XFER_BATCH_DOWNLOAD, filename);
+			continue;
+		}
+		getfilepath(&cfg, &f, path);
+		if(!fexistcase(path)) {
+			bprintf(text[FileDoesNotExist], path);
+			batch_file_remove(&cfg, useron.number, XFER_BATCH_DOWNLOAD, filename);
+		}
+		else {
+#ifdef _WIN32
+			if(!native) {
+				char tmp[MAX_PATH + 1];
+				GetShortPathName(path, tmp, sizeof(tmp));
+				SAFECOPY(path, tmp);
+			}
+#endif
+			fprintf(fp, "%s\r\n", path);
+		}
+		smb_freefilemem(&f);
 	}
-	close(file);
-	return(true);
+	fclose(fp);
+	iniFreeStringList(ini);
+	iniFreeStringList(filenames);
+	return true;
 }
 
 /****************************************************************************/
@@ -529,66 +442,28 @@ bool sbbs_t::create_batchdn_lst(bool native)
 /****************************************************************************/
 bool sbbs_t::create_batchup_lst()
 {
-    char	str[256];
-    int		file;
-	uint	i;
-
-	SAFEPRINTF(str,"%sBATCHUP.LST",cfg.node_dir);
-	if((file=nopen(str,O_WRONLY|O_CREAT|O_TRUNC))==-1) {
-		errormsg(WHERE,ERR_OPEN,str,O_WRONLY|O_CREAT|O_TRUNC);
-		return(false); 
-	}
-	for(i=0;i<batup_total;i++) {
-		if(batup_dir[i]>=cfg.total_dirs)
-			SAFECOPY(str,cfg.temp_dir);
-		else
-			SAFECOPY(str,batup_alt[i]>0 && batup_alt[i]<=cfg.altpaths
-				? cfg.altpath[batup_alt[i]-1] : cfg.dir[batup_dir[i]]->path);
-		write(file,str,strlen(str));
-		unpadfname(batup_name[i],str);
-		strcat(str,crlf);
-		write(file,str,strlen(str)); 
-	}
-	close(file);
-	return(true);
-}
-
-/****************************************************************************/
-/* Creates the file BIMODEM.PTH in the node directory. Returns true if      */
-/* everything goes okay, false if not.                                      */
-/****************************************************************************/
-bool sbbs_t::create_bimodem_pth()
-{
-    char	str[256],tmp2[512];
-	char 	tmp[512];
-    int		file;
-	uint	i;
+    char	path[MAX_PATH + 1];
 
-	SAFEPRINTF(str,"%sBIMODEM.PTH",cfg.node_dir);  /* Create bimodem file */
-	if((file=nopen(str,O_WRONLY|O_CREAT|O_TRUNC))==-1) {
-		errormsg(WHERE,ERR_OPEN,str,O_WRONLY|O_CREAT|O_TRUNC);
-		return(false); 
-	}
-	for(i=0;i<batup_total;i++) {
-		SAFEPRINTF2(str,"%s%s",batup_dir[i]>=cfg.total_dirs ? cfg.temp_dir
-			: batup_alt[i]>0 && batup_alt[i]<=cfg.altpaths
-			? cfg.altpath[batup_alt[i]-1] : cfg.dir[batup_dir[i]]->path
-			,unpadfname(batup_name[i],tmp));
-		SAFEPRINTF2(tmp2,"D       %-80.80s%-160.160s"
-			,unpadfname(batup_name[i],tmp),str);
-		write(file,tmp2,248); 
+	SAFEPRINTF(path,"%sBATCHUP.LST",cfg.node_dir);
+	FILE* fp = fopen(path, "wb");
+	if(fp == NULL) {
+		errormsg(WHERE, ERR_OPEN, path);
+		return false;
 	}
-	for(i=0;i<batdn_total;i++) {
-		SAFEPRINTF2(str,"%s%s"
-			,(batdn_dir[i]>=cfg.total_dirs || cfg.dir[batdn_dir[i]]->seqdev)
-			? cfg.temp_dir : batdn_alt[i]>0 && batdn_alt[i]<=cfg.altpaths
-				? cfg.altpath[batdn_alt[i]-1] : cfg.dir[batdn_dir[i]]->path
-				,unpadfname(batdn_name[i],tmp));
-		SAFEPRINTF(tmp2,"U       %-240.240s",str);
-		write(file,tmp2,248); 
+	str_list_t ini = batch_list_read(&cfg, useron.number, XFER_BATCH_UPLOAD);
+	str_list_t filenames = iniGetSectionList(ini, /* prefix: */NULL);
+	for(size_t i = 0; filenames[i] != NULL; ++i) {
+		const char* filename = filenames[i];
+		file_t f = {{}};
+		if(!batch_file_get(&cfg, ini, filename, &f))
+			continue;
+		fprintf(fp, "%s%s\r\n", cfg.dir[f.dir]->path, filename);
+		smb_freemsgmem(&f);
 	}
-	close(file);
-	return(true);
+	fclose(fp);
+	iniFreeStringList(ini);
+	iniFreeStringList(filenames);
+	return true;
 }
 
 /****************************************************************************/
@@ -596,79 +471,74 @@ bool sbbs_t::create_bimodem_pth()
 /****************************************************************************/
 void sbbs_t::batch_upload()
 {
-    char	str1[MAX_PATH+1],str2[MAX_PATH+1];
-	char	path[MAX_PATH+1];
-	char 	tmp[MAX_PATH+1];
-	uint	i,j,x,y;
-    file_t	f;
-	DIR*	dir;
-	DIRENT*	dirent;
-
-	for(i=0;i<batup_total;) {
-		curdirnum=batup_dir[i]; 			/* for ARS */
-		lncntr=0;                               /* defeat pause */
-		unpadfname(batup_name[i],tmp);
-		SAFEPRINTF2(str1,"%s%s",cfg.temp_dir,tmp);
-		SAFEPRINTF2(str2,"%s%s",cfg.dir[batup_dir[i]]->path,tmp);
-		if(fexistcase(str1) && fexistcase(str2)) { /* file's in two places */
-			bprintf(text[FileAlreadyThere],batup_name[i]);
-			remove(str1);    /* this is the one received */
-			i++;
-			continue; 
-		}
-		if(fexist(str1))
-			mv(str1,str2,0);
-		SAFECOPY(f.name,batup_name[i]);
-		SAFECOPY(f.desc,batup_desc[i]);
-		f.dir=batup_dir[i];
-		f.misc=batup_misc[i];
-		f.altpath=batup_alt[i];
-		if(uploadfile(&f)) {
-			batup_total--;
-			for(j=i;j<batup_total;j++) {
-				batup_dir[j]=batup_dir[j+1];
-				batup_alt[j]=batup_alt[j+1];
-				batup_misc[j]=batup_misc[j+1];
-				strcpy(batup_name[j],batup_name[j+1]);
-				strcpy(batup_desc[j],batup_desc[j+1]); 
-			} 
+	char src[MAX_PATH + 1];
+	char dest[MAX_PATH + 1];
+
+	str_list_t ini = batch_list_read(&cfg, useron.number, XFER_BATCH_UPLOAD);
+	str_list_t filenames = iniGetSectionList(ini, /* prefix: */NULL);
+	for(size_t i = 0; filenames[i] != NULL; ++i) {
+		const char* filename = filenames[i];
+		int dir = batch_file_dir(&cfg, ini, filename);
+		curdirnum = dir; /* for ARS */
+		lncntr = 0; /* defeat pause */
+
+		SAFEPRINTF2(src, "%s%s", cfg.temp_dir, filename);
+		SAFEPRINTF2(dest, "%s%s", cfg.dir[dir]->path, filename);
+		if(fexistcase(src) && fexistcase(dest)) { /* file's in two places */
+			bprintf(text[FileAlreadyThere], filename);
+			remove(src);    /* this is the one received */
+			continue;
 		}
-		else i++; 
+
+		if(fexist(src))
+			mv(src, dest, /* copy: */FALSE);
+
+		file_t f = {{}};
+		if(!batch_file_get(&cfg, ini, filename, &f))
+			continue;
+		if(uploadfile(&f))
+			batch_file_remove(&cfg, useron.number, XFER_BATCH_DOWNLOAD, filename);
+		smb_freemsgmem(&f);
 	}
-	if(cfg.upload_dir==INVALID_DIR)
+	iniFreeStringList(filenames);
+	iniFreeStringList(ini);
+
+	if(cfg.upload_dir == INVALID_DIR) // no blind upload dir specified
 		return;
-	dir=opendir(cfg.temp_dir);
+
+	DIR* dir = opendir(cfg.temp_dir);
+	DIRENT* dirent;
 	while(dir!=NULL && (dirent=readdir(dir))!=NULL) {
-		SAFEPRINTF2(tmp,"%s%s",cfg.temp_dir,dirent->d_name);
-		if(isdir(tmp))
+		SAFEPRINTF2(src, "%s%s", cfg.temp_dir,dirent->d_name);
+		if(isdir(src))
 			continue;
-		memset(&f,0,sizeof(file_t));
-		f.dir=cfg.upload_dir;
-		SAFEPRINTF2(path,"%s%s",cfg.dir[f.dir]->path,dirent->d_name);
-		if(fexistcase(path)) {
+		SAFEPRINTF2(dest, "%s%s", cfg.dir[cfg.upload_dir]->path, dirent->d_name);
+		if(fexistcase(dest)) {
 			bprintf(text[FileAlreadyOnline], dirent->d_name);
 			continue;
 		}
-		if(mv(tmp, path, /* copy: */false))
+		if(mv(src, dest, /* copy: */false))
 			continue;
 
-#ifdef _WIN32
-		GetShortPathName(path, tmp, sizeof(tmp));
-#endif
-		padfname(getfname(tmp),f.name);
-
+		bputs(text[SearchingForDupes]);
+		uint x,y;
 		for(x=0;x<usrlibs;x++) {
 			for(y=0;y<usrdirs[x];y++)
 				if(cfg.dir[usrdir[x][y]]->misc&DIR_DUPES
-					&& findfile(&cfg,usrdir[x][y],f.name))
+					&& findfile(&cfg,usrdir[x][y], dirent->d_name, NULL))
 					break;
 			if(y<usrdirs[x])
 				break; 
 		}
+		bputs(text[SearchedForDupes]);
 		if(x<usrlibs) {
-			bprintf(text[FileAlreadyOnline],f.name);
+			bprintf(text[FileAlreadyOnline], dirent->d_name);
 		} else {
-			uploadfile(&f); 
+			file_t f = {{}};
+			f.dir = cfg.upload_dir;
+			smb_hfield_str(&f, SMB_FILENAME, dirent->d_name);
+			uploadfile(&f);
+			smb_freefilemem(&f);
 		}
 	}
 	if(dir!=NULL)
@@ -681,33 +551,32 @@ void sbbs_t::batch_upload()
 /****************************************************************************/
 void sbbs_t::batch_download(int xfrprot)
 {
-    uint	i,j;
-    file_t	f;
+	FILE* fp = batch_list_open(&cfg, useron.number, XFER_BATCH_DOWNLOAD, /* create: */FALSE);
+	if(fp == NULL)
+		return;
+	str_list_t ini = iniReadFile(fp);
+	str_list_t filenames = iniGetSectionList(ini, /* prefix: */NULL);
 
-	for(i=0;i<batdn_total;) {
+	for(size_t i = 0; filenames[i] != NULL; ++i) {
+		char* filename = filenames[i];
 		lncntr=0;                               /* defeat pause */
-		f.dir=curdirnum=batdn_dir[i];
-		SAFECOPY(f.name,batdn_name[i]);
-		f.datoffset=batdn_offset[i];
-		f.size=batdn_size[i];
-		f.altpath=batdn_alt[i];
-		if(xfrprot==-1 || checkprotresult(cfg.prot[xfrprot],0,&f)) {
+		if(xfrprot==-1 || checkprotresult(cfg.prot[xfrprot], 0, filename)) {
+			file_t f = {{}};
+			if(!batch_file_load(&cfg, ini, filename, &f)) {
+				errormsg(WHERE, "loading file", filename, i);
+				continue;
+			}
+			iniRemoveSection(&ini, filename);
 			if(cfg.dir[f.dir]->misc&DIR_TFREE && cur_cps)
 				starttime+=f.size/(ulong)cur_cps;
-			downloadfile(&f);
-			closefile(&f);
-			batdn_total--;
-			for(j=i;j<batdn_total;j++) {
-				strcpy(batdn_name[j],batdn_name[j+1]);
-				batdn_dir[j]=batdn_dir[j+1];
-				batdn_cdt[j]=batdn_cdt[j+1];
-				batdn_alt[j]=batdn_alt[j+1];
-				batdn_size[j]=batdn_size[j+1];
-				batdn_offset[j]=batdn_offset[j+1]; 
-			} 
+			downloadedfile(&f);
+			smb_freefilemem(&f);
 		}
-		else i++; 
 	}
+	iniWriteFile(fp, ini);
+	iniCloseFile(fp);
+	iniFreeStringList(ini);
+	iniFreeStringList(filenames);
 }
 
 /****************************************************************************/
@@ -715,7 +584,8 @@ void sbbs_t::batch_download(int xfrprot)
 /****************************************************************************/
 void sbbs_t::batch_add_list(char *list)
 {
-    char	str[128];
+    char	str[1024];
+	char	path[MAX_PATH + 1];
 	int		file;
 	uint	i,j,k;
     FILE *	stream;
@@ -727,52 +597,31 @@ void sbbs_t::batch_add_list(char *list)
 			checkline();
 			if(!online)
 				break;
-			if(!fgets(str,127,stream))
+			if(!fgets(str, sizeof(str) - 1,stream))
 				break;
 			truncnl(str);
-			sprintf(f.name,"%.12s",str);
 			lncntr=0;
 			for(i=j=k=0;i<usrlibs;i++) {
 				for(j=0;j<usrdirs[i];j++,k++) {
 					outchar('.');
 					if(k && !(k%5))
 						bputs("\b\b\b\b\b     \b\b\b\b\b");
-					if(findfile(&cfg,usrdir[i][j],f.name))
-						break; 
+					if(loadfile(&cfg, usrdir[i][j], str, &f, file_detail_normal)) {
+						if(fexist(getfilepath(&cfg, &f, path)))
+							addtobatdl(&f);
+						else
+							bprintf(text[FileIsNotOnline],f.name);
+						smb_freefilemem(&f);
+						break;
+					}
 				}
 				if(j<usrdirs[i])
 					break; 
 			}
-			if(i<usrlibs) {
-				f.dir=usrdir[i][j];
-				getfileixb(&cfg,&f);
-				f.size=0;
-				getfiledat(&cfg,&f);
-				if(f.size==-1L)
-					bprintf(text[FileIsNotOnline],f.name);
-				else
-					addtobatdl(&f); 
-			} 
 		}
 		fclose(stream);
 		remove(list);
-		CRLF; 
-	}
-}
-/****************************************************************************/
-void sbbs_t::batch_create_list()
-{
-	char	str[MAX_PATH+1];
-	int		i;
-	FILE*	stream;
-
-	if(batdn_total) {
-		SAFEPRINTF2(str,"%sfile/%04u.dwn",cfg.data_dir,useron.number);
-		if((stream=fnopen(NULL,str,O_WRONLY|O_TRUNC|O_CREAT))!=NULL) {
-			for(i=0;i<(int)batdn_total;i++)
-				fprintf(stream,"%s\r\n",batdn_name[i]);
-			fclose(stream); 
-		} 
+		CRLF;
 	}
 }
 
@@ -784,68 +633,97 @@ bool sbbs_t::addtobatdl(file_t* f)
     char	str[256],tmp2[256];
 	char 	tmp[512];
     uint	i;
-	ulong	totalcdt, totalsize, totaltime;
+	uint64_t	totalcost, totalsize;
+	uint64_t	totaltime;
 
 	if(useron.rest&FLAG('D')) {
 		bputs(text[R_Download]);
-		return(false); 
-	}
-	for(i=0;i<batdn_total;i++) {
-		if(!strcmp(batdn_name[i],f->name) && f->dir==batdn_dir[i]) {
-			bprintf(text[FileAlreadyInQueue],f->name);
-			return(false); 
-		} 
-	}
-	if(f->size<=0 /* !fexist(str) */) {
-		bprintf(text[CantAddToQueue],f->name);
-		bprintf(text[FileIsNotOnline],f->name);
-		return(false); 
-	}
-	if(batdn_total>=cfg.max_batdn) {
-		bprintf(text[CantAddToQueue],f->name);
-		bputs(text[BatchDlQueueIsFull]);
-		return(false); 
-	}
-	for(i=0,totalcdt=0;i<batdn_total;i++)
-		if(!is_download_free(&cfg,batdn_dir[i],&useron,&client))
-			totalcdt+=batdn_cdt[i];
-	if(cfg.dir[f->dir]->misc&DIR_FREE) f->cdt=0L;
-	if(!is_download_free(&cfg,f->dir,&useron,&client))
-		totalcdt+=f->cdt;
-	if(totalcdt>useron.cdt+useron.freecdt) {
-		bprintf(text[CantAddToQueue],f->name);
-		bprintf(text[YouOnlyHaveNCredits],ultoac(useron.cdt+useron.freecdt,tmp));
-		return(false); 
+		return false;
 	}
 	if(!chk_ar(cfg.dir[f->dir]->dl_ar,&useron,&client)) {
 		bprintf(text[CantAddToQueue],f->name);
 		bputs(text[CantDownloadFromDir]);
-		return(false); 
+		return false;
 	}
-	for(i=0,totalsize=totaltime=0;i<batdn_total;i++) {
-		totalsize+=batdn_size[i];
-		if(!(cfg.dir[batdn_dir[i]]->misc&DIR_TFREE) && cur_cps)
-			totaltime+=batdn_size[i]/(ulong)cur_cps; 
+	if(getfilesize(&cfg, f) < 1) {
+		bprintf(text[CantAddToQueue], f->name);
+		bprintf(text[FileIsNotOnline], f->name);
+		return false;
 	}
-	totalsize+=f->size;
-	if(!(cfg.dir[f->dir]->misc&DIR_TFREE) && cur_cps)
-		totaltime+=f->size/(ulong)cur_cps;
-	if(!(useron.exempt&FLAG('T')) && totaltime>timeleft) {
-		bprintf(text[CantAddToQueue],f->name);
-		bputs(text[NotEnoughTimeToDl]);
-		return(false); 
+
+	str_list_t ini = batch_list_read(&cfg, useron.number, XFER_BATCH_DOWNLOAD);
+	if(iniSectionExists(ini, f->name)) {
+		bprintf(text[FileAlreadyInQueue], f->name);
+		iniFreeStringList(ini);
+		return false;
 	}
-	strcpy(batdn_name[batdn_total],f->name);
-	batdn_dir[batdn_total]=f->dir;
-	batdn_cdt[batdn_total]=f->cdt;
-	batdn_offset[batdn_total]=f->datoffset;
-	batdn_size[batdn_total]=f->size;
-	batdn_alt[batdn_total]=f->altpath;
-	batdn_total++;
-	openfile(f);
-	bprintf(text[FileAddedToBatDlQueue]
-		,f->name,batdn_total,cfg.max_batdn,ultoac(totalcdt,tmp)
-		,ultoac(totalsize,tmp2)
-		,sectostr(totalsize/(ulong)cur_cps,str));
-	return(true);
+
+	bool result = false;
+	str_list_t filenames = iniGetSectionList(ini, /* prefix: */NULL);
+	if(strListCount(filenames) >= cfg.max_batdn) {
+		bprintf(text[CantAddToQueue] ,f->name);
+		bputs(text[BatchDlQueueIsFull]);
+	} else {
+		totalcost = 0;
+		totaltime = 0;
+		totalsize = 0;
+		for(i=0; filenames[i] != NULL; ++i) {
+			const char* filename = filenames[i];
+			file_t bf = {{}};
+			if(!batch_file_load(&cfg, ini, filename, &bf))
+				continue;
+			totalcost += bf.cost;
+			totalsize += getfilesize(&cfg, &bf);
+			if(!(cfg.dir[bf.dir]->misc&DIR_TFREE) && cur_cps)
+				totaltime += bf.size/(ulong)cur_cps;
+			smb_freefilemem(&bf);
+		}
+		if(cfg.dir[f->dir]->misc&DIR_FREE)
+			f->cost=0L;
+		if(!is_download_free(&cfg,f->dir,&useron,&client))
+			totalcost += f->cost;
+		if(totalcost > useron.cdt+useron.freecdt) {
+			bprintf(text[CantAddToQueue],f->name);
+			bprintf(text[YouOnlyHaveNCredits],ultoac(useron.cdt+useron.freecdt,tmp));
+		} else {
+			totalsize += f->size;
+			if(!(cfg.dir[f->dir]->misc&DIR_TFREE) && cur_cps)
+				totaltime += f->size/(ulong)cur_cps;
+			if(!(useron.exempt&FLAG('T')) && totaltime > timeleft) {
+				bprintf(text[CantAddToQueue],f->name);
+				bputs(text[NotEnoughTimeToDl]);
+			} else {
+				if(batch_file_add(&cfg, useron.number, XFER_BATCH_DOWNLOAD, f)) {
+					bprintf(text[FileAddedToBatDlQueue]
+						,f->name, strListCount(filenames) + 1, cfg.max_batdn, ultoac((ulong)totalcost,tmp)
+						,ultoac((ulong)totalsize,tmp2)
+						,sectostr((ulong)totalsize/(ulong)cur_cps,str));
+					result = true;
+				}
+			}
+		}
+	}
+	iniFreeStringList(ini);
+	iniFreeStringList(filenames);
+	return result;
+}
+
+bool sbbs_t::clearbatdl(void)
+{
+	return batch_list_clear(&cfg, useron.number, XFER_BATCH_DOWNLOAD);
+}
+
+bool sbbs_t::clearbatul(void)
+{
+	return batch_list_clear(&cfg, useron.number, XFER_BATCH_UPLOAD);
+}
+
+size_t sbbs_t::batdn_total(void)
+{
+	return batch_file_count(&cfg, useron.number, XFER_BATCH_DOWNLOAD);
+}
+
+size_t sbbs_t::batup_total(void)
+{
+	return batch_file_count(&cfg, useron.number, XFER_BATCH_UPLOAD);
 }
diff --git a/src/sbbs3/chksmb.c b/src/sbbs3/chksmb.c
index f2ec6dca7c40e270137d433363330db4e059bbed..cd289af702f75fc82eb21c91bbea1f819be4707f 100644
--- a/src/sbbs3/chksmb.c
+++ b/src/sbbs3/chksmb.c
@@ -1,8 +1,5 @@
 /* Synchronet message base (SMB) validity checker */
 
-/* $Id: chksmb.c,v 1.72 2020/04/04 20:36:38 rswindell Exp $ */
-// vi: tabstop=4
-
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
  * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
@@ -16,21 +13,9 @@
  * See the GNU General Public License for more details: gpl.txt or			*
  * http://www.fsf.org/copyleft/gpl.html										*
  *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
- *																			*
  * For Synchronet coding style and modification guidelines, see				*
  * http://www.synchro.net/source.html										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
@@ -41,6 +26,9 @@
 #include <time.h>		/* ctime */
 #include <ctype.h>		/* toupper */
 
+#include "git_branch.h"
+#include "git_hash.h"
+
 /* SMB-specific */
 #include "genwrap.h"
 #include "conwrap.h"	/* getch */
@@ -117,13 +105,21 @@ BOOL contains_ctrl_chars(char* str)
 
 void print_hash(hash_t* hash)
 {
+	char str[128];
+
 	printf("\t%-20s = %lu\n"		,"hash.number"	, (ulong)hash->number);
 	printf("\t%-20s = 0x%08lX\n"	,"hash.time"	, (ulong)hash->time);
 	printf("\t%-20s = %lu\n"		,"hash.length"	, (ulong)hash->length);
 	printf("\t%-20s = 0x%02X\n"		,"hash.source"	, (unsigned)hash->source);
 	printf("\t%-20s = 0x%02X\n"		,"hash.flags"	, (unsigned)hash->flags);
-	printf("\t%-20s = 0x%04hX\n"	,"hash.crc16"	, hash->crc16);
-	printf("\t%-20s = 0x%08X\n"		,"hash.crc32"	, hash->crc32);
+	if(hash->flags & SMB_HASH_CRC16)
+		printf("\t%-20s = 0x%04hX\n"	,"hash.crc16"	, hash->data.crc16);
+	if(hash->flags & SMB_HASH_CRC32)
+		printf("\t%-20s = 0x%08X\n"		,"hash.crc32"	, hash->data.crc32);
+	if(hash->flags & SMB_HASH_MD5)
+		printf("\t%-20s = %s\n"			,"hash.md5"		, MD5_hex(str, hash->data.md5));
+	if(hash->flags & SMB_HASH_SHA1)
+		printf("\t%-20s = %s\n"			,"hash.sha1"	, SHA1_hex(str, hash->data.sha1));
 }
 
 char *usage="\nusage: chksmb [-opts] <filespec.SHD>\n"
@@ -175,15 +171,13 @@ int main(int argc, char **argv)
 	smb_t		smb;
 	idxrec_t	idx;
 	idxrec_t*	idxrec = NULL;
+	fileidxrec_t* fidxrec = NULL;
 	smbmsg_t	msg;
 	hash_t**	hashes;
-	char		revision[16];
 	time_t		now=time(NULL);
 
-	sscanf("$Revision: 1.72 $", "%*s %s", revision);
-
-	fprintf(stderr,"\nCHKSMB v2.30-%s (rev %s) SMBLIB %s - Check Synchronet Message Base\n"
-		,PLATFORM_DESC,revision,smb_lib_ver());
+	fprintf(stderr,"\nCHKSMB v3.19-%s %s/%s SMBLIB %s - Check Synchronet Message Base\n"
+		,PLATFORM_DESC, GIT_BRANCH, GIT_HASH, smb_lib_ver());
 
 	if(argc<2) {
 		printf("%s",usage);
@@ -277,32 +271,34 @@ int main(int argc, char **argv)
 		continue;
 	}
 
+	size_t idxreclen = smb_idxreclen(&smb);
 	off_t sid_length = filelength(fileno(smb.sid_fp));
-	if(sid_length != smb.status.total_msgs * sizeof(idxrec_t)) {
-		printf("!Size of index file (%ld) is incorrect (expected: %ld)\n", sid_length, (long)(smb.status.total_msgs * sizeof(idxrec_t)));
+	if(sid_length != smb.status.total_msgs * idxreclen) {
+		printf("!Size of index file (%ld) is incorrect (expected: %ld)\n", (long)sid_length, (long)(smb.status.total_msgs * idxreclen));
 		smb_close(&smb);
 		errors++;
 		continue;
 	}
 
 	FREE_AND_NULL(idxrec);
-	if((idxrec = calloc(smb.status.total_msgs, sizeof(*idxrec))) == NULL) {
+	if((idxrec = calloc(smb.status.total_msgs, idxreclen)) == NULL) {
 		printf("!Error allocating %lu index record\n", (ulong)smb.status.total_msgs);
 		smb_close(&smb);
 		errors++;
 		continue;
 	}
-	if(fread(idxrec, sizeof(*idxrec), smb.status.total_msgs, smb.sid_fp) != smb.status.total_msgs) {
+	if(fread(idxrec, idxreclen, smb.status.total_msgs, smb.sid_fp) != smb.status.total_msgs) {
 		printf("!Error reading %lu index records\n", (ulong)smb.status.total_msgs);
 		smb_close(&smb);
 		errors++;
 		continue;
 	}
+	fidxrec = (fileidxrec_t*)idxrec;
 
 	off_t shd_hdrs = shd_length - smb.status.header_offset;
 
 	if(shd_hdrs && (shd_hdrs%SHD_BLOCK_LEN) != 0)
-		printf("!Size of msg header records in SHD file incorrect: %lu\n", shd_hdrs);
+		printf("!Size of msg header records in SHD file incorrect: %lu\n", (ulong)shd_hdrs);
 
 	if((i=smb_locksmbhdr(&smb))!=0) {
 		smb_close(&smb);
@@ -311,21 +307,21 @@ int main(int argc, char **argv)
 		continue;
 	}
 
-	if(((shd_hdrs/SHD_BLOCK_LEN)*sizeof(ulong)) != 0) {
-		if((number=malloc(((shd_hdrs/SHD_BLOCK_LEN)+2)*sizeof(ulong)))
+	if(((shd_hdrs/SHD_BLOCK_LEN)*sizeof(*number)) != 0){
+		if((number=malloc((size_t)(((shd_hdrs/SHD_BLOCK_LEN)+2))*sizeof(*number)))
 			==NULL) {
 			printf("Error allocating %lu bytes of memory\n"
-				,(shd_hdrs/SHD_BLOCK_LEN)*sizeof(ulong));
-			return(++errors);
-		}
+				,(ulong)((shd_hdrs/SHD_BLOCK_LEN)*sizeof(*number)));
+			return(++errors); 
+		} 
 	}
 	else
 		number=NULL;
 
 	off_t sdt_length = filelength(fileno(smb.sdt_fp));
 	if(sdt_length && (sdt_length % SDT_BLOCK_LEN) != 0)
-		printf("!Size of SDT file (%lu) not evenly divisble by block length (%u)\n"
-			,sdt_length, SDT_BLOCK_LEN);
+		printf("!Size of SDT file (%lu) not evenly divisible by block length (%u)\n"
+			,(ulong)sdt_length, SDT_BLOCK_LEN);
 
 	if(chkalloc && !(smb.status.attr&SMB_HYPERALLOC)) {
 		if((i=smb_open_ha(&smb))!=0) {
@@ -335,8 +331,8 @@ int main(int argc, char **argv)
 		if(filelength(fileno(smb.shd_fp)) != smb.status.header_offset
 			+ (filelength(fileno(smb.sha_fp)) * SHD_BLOCK_LEN))
 			printf("!Size of SHA file (%lu) does not match SHD file (%lu)\n"
-				,filelength(fileno(smb.sha_fp))
-				,filelength(fileno(smb.shd_fp)));
+				,(ulong)filelength(fileno(smb.sha_fp))
+				,(ulong)filelength(fileno(smb.shd_fp)));
 
 		if((i=smb_open_da(&smb))!=0) {
 			printf("smb_open_da returned %d: %s\n",i,smb.last_error);
@@ -344,8 +340,8 @@ int main(int argc, char **argv)
 		}
 		if((filelength(fileno(smb.sda_fp)))/sizeof(uint16_t) != filelength(fileno(smb.sdt_fp))/SDT_BLOCK_LEN)
 			printf("!Size of SDA file (%lu) does not match SDT file (%lu)\n"
-				,filelength(fileno(smb.sda_fp))
-				,filelength(fileno(smb.sdt_fp)));
+				,(ulong)filelength(fileno(smb.sda_fp))
+				,(ulong)filelength(fileno(smb.sdt_fp)));
 	}
 
 	headers=deleted=orphan=dupenumhdr=attr=zeronum=timeerr=lockerr=hdrerr=0;
@@ -402,15 +398,23 @@ int main(int argc, char **argv)
 		smb_unlockmsghdr(&smb,&msg);
 		size=smb_hdrblocks(smb_getmsghdrlen(&msg))*SHD_BLOCK_LEN;
 
-		SAFECOPY(from,msg.from);
+		if(msg.hdr.type == SMB_MSG_TYPE_FILE)
+			SAFECOPY(from, msg.subj);
+		else
+			SAFECOPY(from, msg.from);
 		truncsp(from);
 		strip_ctrl(from);
 		fprintf(stderr,"#%-5"PRIu32" (%06lX) %-25.25s ",msg.hdr.number,l,from);
 
 		for(n = 0; n < smb.status.total_msgs; n++) {
-			if(idxrec[n].number == msg.hdr.number)
+			idxrec_t* idx;
+			if(idxreclen == sizeof(*fidxrec))
+				idx = &fidxrec[n].idx;
+			else
+				idx = &idxrec[n];
+			if(idx->number == msg.hdr.number)
 				continue;
-			if(idxrec[n].offset > l && idxrec[n].offset < l + (smb_hdrblocks(msg.hdr.length) * SHD_BLOCK_LEN)) {
+			if(idx->offset > l && idx->offset < l + (smb_hdrblocks(msg.hdr.length) * SHD_BLOCK_LEN)) {
 				fprintf(stderr,"%sMessage header overlap\n", beep);
 				msgerr=TRUE;
 				if(extinfo)
@@ -440,7 +444,7 @@ int main(int argc, char **argv)
 			hdrlenerr++;
 		}
 
-		if(chk_msgids && msg.from_net.type == NET_NONE && msg.id == NULL) {
+		if(msg.hdr.type == SMB_MSG_TYPE_NORMAL && chk_msgids && msg.from_net.type == NET_NONE && msg.id == NULL) {
 			fprintf(stderr,"%sNo Message-ID\n",beep);
 			msgerr=TRUE;
 			if(extinfo)
@@ -479,7 +483,7 @@ int main(int argc, char **argv)
 			types++;
 		}
 
-		if(!(smb.status.attr&SMB_EMAIL) && chkhash) {
+		if(!(smb.status.attr&(SMB_EMAIL|SMB_NOHASH|SMB_FILE_DIRECTORY)) && chkhash) {
 			/* Look-up the message hashes */
 			hashes=smb_msghashes(&msg,(uchar*)body,SMB_HASH_SOURCE_DUPE);
 			if(hashes!=NULL
@@ -503,11 +507,13 @@ int main(int argc, char **argv)
 						printf("%-10s: %"PRIu32"\n",		"Length",	hashes[h]->length);
 						printf("%-10s: %x\n",		"Flags",	hashes[h]->flags);
 						if(hashes[h]->flags&SMB_HASH_CRC16)
-							printf("%-10s: %04x\n",	"CRC-16",	hashes[h]->crc16);
+							printf("%-10s: %04x\n",	"CRC-16",	hashes[h]->data.crc16);
 						if(hashes[h]->flags&SMB_HASH_CRC32)
-							printf("%-10s: %08"PRIx32"\n","CRC-32",	hashes[h]->crc32);
+							printf("%-10s: %08"PRIx32"\n","CRC-32",	hashes[h]->data.crc32);
 						if(hashes[h]->flags&SMB_HASH_MD5)
-							printf("%-10s: %s\n",	"MD5",		MD5_hex((BYTE*)str,hashes[h]->md5));
+							printf("%-10s: %s\n",	"MD5",		MD5_hex(str,hashes[h]->data.md5));
+						if(hashes[h]->flags&SMB_HASH_SHA1)
+							printf("%-10s: %s\n",	"SHA-1",	SHA1_hex(str,hashes[h]->data.md5));
 
 #endif
 					}
@@ -571,7 +577,7 @@ int main(int argc, char **argv)
 							"index import date/time\n");
 					timeerr++;
 				}
-				if(msg.hdr.type != SMB_MSG_TYPE_BALLOT
+				if((msg.hdr.type == SMB_MSG_TYPE_NORMAL || msg.hdr.type == SMB_MSG_TYPE_POLL)
 					&& msg.idx.subj!=smb_subject_crc(msg.subj)) {
 					fprintf(stderr,"%sSubject CRC mismatch\n",beep);
 					msgerr=TRUE;
@@ -581,7 +587,7 @@ int main(int argc, char **argv)
 							,smb_subject_crc(msg.subj),msg.idx.subj);
 					subjcrc++;
 				}
-				if(smb.status.attr&SMB_EMAIL
+				if(smb.status.attr & SMB_EMAIL
 					&& (msg.from_ext!=NULL || msg.idx.from)
 					&& (msg.from_ext==NULL || msg.idx.from!=atoi(msg.from_ext))) {
 					fprintf(stderr,"%sFrom extension mismatch\n",beep);
@@ -592,8 +598,8 @@ int main(int argc, char **argv)
 							,msg.from_ext,msg.idx.from);
 					fromcrc++;
 				}
-				if(!(smb.status.attr&SMB_EMAIL)
-					&& msg.hdr.type != SMB_MSG_TYPE_BALLOT
+				if(!(smb.status.attr & SMB_EMAIL)
+					&& (msg.hdr.type == SMB_MSG_TYPE_NORMAL || msg.hdr.type == SMB_MSG_TYPE_POLL)
 					&& msg.idx.from!=smb_name_crc(msg.from)) {
 					fprintf(stderr,"%sFrom CRC mismatch\n",beep);
 					msgerr=TRUE;
@@ -603,7 +609,7 @@ int main(int argc, char **argv)
 							,smb_name_crc(msg.from),msg.idx.from);
 					fromcrc++;
 				}
-				if(smb.status.attr&SMB_EMAIL
+				if(smb.status.attr & SMB_EMAIL
 					&& (msg.to_ext!=NULL || msg.idx.to)
 					&& (msg.to_ext==NULL || msg.idx.to!=atoi(msg.to_ext))) {
 					fprintf(stderr,"%sTo extension mismatch\n",beep);
@@ -614,8 +620,8 @@ int main(int argc, char **argv)
 							,msg.to_ext,msg.idx.to);
 					tocrc++;
 				}
-				if(!(smb.status.attr&SMB_EMAIL)
-					&& msg.hdr.type != SMB_MSG_TYPE_BALLOT
+				if(!(smb.status.attr & SMB_EMAIL)
+					&& (msg.hdr.type == SMB_MSG_TYPE_NORMAL || msg.hdr.type == SMB_MSG_TYPE_POLL)
 					&& msg.to_ext==NULL && msg.idx.to!=smb_name_crc(msg.to)) {
 					fprintf(stderr,"%sTo CRC mismatch\n",beep);
 					msgerr=TRUE;
@@ -807,7 +813,7 @@ int main(int argc, char **argv)
 		fprintf(stderr,"\r%79s\r100%%\n","");
 	}
 
-	total=filelength(fileno(smb.sid_fp))/sizeof(idxrec_t);
+	total=(ulong)filelength(fileno(smb.sid_fp))/smb_idxreclen(&smb);
 
 	dupenum=dupeoff=misnumbered=idxzeronum=idxnumerr=idxofferr=idxerr=delidx=0;
 
@@ -815,19 +821,19 @@ int main(int argc, char **argv)
 
 	fprintf(stderr,"\nChecking %s Index\n\n",smb.file);
 
-	if((offset=(ulong *)malloc(total*sizeof(ulong)))==NULL) {
-		printf("Error allocating %lu bytes of memory\n",total*sizeof(ulong));
-		return(++errors);
+	if((offset=malloc(total*sizeof(*offset)))==NULL) {
+		printf("Error allocating %lu bytes of memory\n",total*sizeof(*offset));
+		return(++errors); 
 	}
-	if((number=(ulong *)malloc(total*sizeof(ulong)))==NULL) {
-		printf("Error allocating %lu bytes of memory\n",total*sizeof(ulong));
-		return(++errors);
+	if((number=malloc(total*sizeof(*number)))==NULL) {
+		printf("Error allocating %lu bytes of memory\n",total*sizeof(*number));
+		return(++errors); 
 	}
-	fseek(smb.sid_fp,0L,SEEK_SET);
 
 	for(l=0;l<total;l++) {
+		fseek(smb.sid_fp, l * smb_idxreclen(&smb), SEEK_SET);
 		fprintf(stderr,"\r%2lu%%  %5lu ",l ? (long)(100.0/((float)total/l)) : 0,l);
-		if(!fread(&idx,sizeof(idxrec_t),1,smb.sid_fp))
+		if(!fread(&idx,sizeof(idx),1,smb.sid_fp))
 			break;
 		fprintf(stderr,"#%-5"PRIu32" (%06"PRIX32") 1st Pass ",idx.number,idx.offset);
 		if(idx.attr&MSG_DELETE) {
@@ -1042,11 +1048,11 @@ int main(int argc, char **argv)
 			,subjcrc);
 	if(fromcrc)
 		printf("%-35.35s (!): %lu\n"
-			,smb.status.attr&SMB_EMAIL ? INDXERR "From Ext" : INDXERR "From CRCs"
+			,smb.status.attr&(SMB_EMAIL|SMB_FILE_DIRECTORY) ? INDXERR "From Ext" : INDXERR "From CRCs"
 			,fromcrc);
 	if(tocrc)
 		printf("%-35.35s (!): %lu\n"
-			,smb.status.attr&SMB_EMAIL ? INDXERR "To Ext" : INDXERR "To CRCs"
+			,smb.status.attr&(SMB_EMAIL|SMB_FILE_DIRECTORY) ? INDXERR "To Ext" : INDXERR "To CRCs"
 			,tocrc);
 	if(intransit)
 		printf("%-35.35s (?): %lu\n"
diff --git a/src/sbbs3/con_out.cpp b/src/sbbs3/con_out.cpp
index 80fbac5d1021f1948af830028097cbf996474ee7..8f72f84f9cb075e8c863c29043ed479dff331400 100644
--- a/src/sbbs3/con_out.cpp
+++ b/src/sbbs3/con_out.cpp
@@ -1163,18 +1163,13 @@ void sbbs_t::ctrl_a(char x)
 			cursor_left();
 			break;
 		case '/':	/* Conditional new-line */
-			if(column > 0)
-				newline();
+			cond_newline();
 			break;
 		case '\\':	/* Conditional New-line / Continuation prefix (if cols < 80) */
-			if(column > 0 && cols < TERM_COLS_DEFAULT)
-				bputs(text[LongLineContinuationPrefix]);
+			cond_contline();
 			break;
 		case '?':	/* Conditional blank-line */
-			if(column > 0)
-				newline();
-			if(lastlinelen)
-				newline();
+			cond_blankline();
 			break;
 		case '[':   /* Carriage return */
 			carriage_return();
diff --git a/src/sbbs3/ctrl/AboutBoxFormUnit.dfm b/src/sbbs3/ctrl/AboutBoxFormUnit.dfm
index 6c7d2babbad9d87b6dae541d10e96310500d2c4f..2332f3c8720755c453aa8d9a1b37dd02ccbad9fc 100644
--- a/src/sbbs3/ctrl/AboutBoxFormUnit.dfm
+++ b/src/sbbs3/ctrl/AboutBoxFormUnit.dfm
@@ -13373,7 +13373,7 @@ object AboutBoxForm: TAboutBoxForm
     Alignment = taRightJustify
     Anchors = [akLeft, akBottom]
     AutoSize = False
-    Caption = 'Copyright 2020  ::'
+    Caption = 'Copyright 2021  ::'
     Font.Charset = DEFAULT_CHARSET
     Font.Color = clWindowText
     Font.Height = -15
diff --git a/src/sbbs3/ctrl/MainFormUnit.cpp b/src/sbbs3/ctrl/MainFormUnit.cpp
index e0110b876207485daf1f212d695db5c6e667afb2..a6adb7854505710794c5d1e7b48a52eaf6ab1296 100644
--- a/src/sbbs3/ctrl/MainFormUnit.cpp
+++ b/src/sbbs3/ctrl/MainFormUnit.cpp
@@ -293,9 +293,6 @@ static int lputs(void* p, int level, const char *str)
 
 static void log_msg(TRichEdit* Log, log_msg_t* msg)
 {
-    while(MaxLogLen && Log->Lines->Count >= MaxLogLen)
-        Log->Lines->Delete(0);
-
     AnsiString Line=SystemTimeToDateTime(msg->time).FormatString(LOG_TIME_FMT)+"  ";
     Line+=AnsiString(msg->buf).Trim();
 	if(msg->repeated)
@@ -305,7 +302,13 @@ static void log_msg(TRichEdit* Log, log_msg_t* msg)
     Log->SelAttributes->Assign(
         MainForm->LogAttributes(msg->level, Log->Color, Log->Font));
 	Log->Lines->Add(Line);
-    SendMessage(Log->Handle, WM_VSCROLL, SB_BOTTOM, NULL);
+}
+
+static void logged_msgs(TRichEdit* Log)
+{
+    while(MaxLogLen && Log->Lines->Count >= MaxLogLen)
+        Log->Lines->Delete(0);
+	SendMessage(Log->Handle, WM_VSCROLL, SB_BOTTOM, NULL);
 }
 
 static void bbs_log_msg(log_msg_t* msg)
@@ -3393,60 +3396,86 @@ void __fastcall TMainForm::LogTimerTick(TObject *Sender)
 {
     log_msg_t   msg;
     log_msg_t*  pmsg;
+	ulong count;
+	const int max_lines = 1000;
 
     if(!TelnetPause->Checked) {
-        while(GetServerLogLine(bbs_log,NTSVC_NAME_BBS,&msg))
-            bbs_log_msg(&msg);
+		count = 0;
+        while(count < max_lines && GetServerLogLine(bbs_log,NTSVC_NAME_BBS,&msg))
+            bbs_log_msg(&msg), count++;
 
-        while(GetServerLogLine(event_log,NTSVC_NAME_EVENT,&msg))
-            event_log_msg(&msg);
-
-        while((pmsg=(log_msg_t*)listShiftNode(&bbs_log_list)) != NULL) {
+        while(count < max_lines && (pmsg=(log_msg_t*)listShiftNode(&bbs_log_list)) != NULL) {
             bbs_log_msg(pmsg);
             free(pmsg);
+			count++;
         }
+		if(count)
+			logged_msgs(TelnetForm->Log);
+
+		count = 0;
+        while(count < max_lines && GetServerLogLine(event_log,NTSVC_NAME_EVENT,&msg))
+            event_log_msg(&msg), count++;
 
-        while((pmsg=(log_msg_t*)listShiftNode(&event_log_list)) != NULL) {
+        while(count < max_lines && (pmsg=(log_msg_t*)listShiftNode(&event_log_list)) != NULL) {
             event_log_msg(pmsg);
             free(pmsg);
+			count++;
         }
+		if(count)
+			logged_msgs(EventsForm->Log);
     }
 
     if(!FtpPause->Checked) {
-        while(GetServerLogLine(ftp_log,NTSVC_NAME_FTP,&msg))
-            ftp_log_msg(&msg);
+		count = 0;
+        while(count < max_lines && GetServerLogLine(ftp_log,NTSVC_NAME_FTP,&msg))
+            ftp_log_msg(&msg), count++;
 
-        while((pmsg=(log_msg_t*)listShiftNode(&ftp_log_list)) != NULL) {
+        while(count < max_lines && (pmsg=(log_msg_t*)listShiftNode(&ftp_log_list)) != NULL) {
             ftp_log_msg(pmsg);
             free(pmsg);
+			count++;
         }
+		if(count)
+			logged_msgs(FtpForm->Log);
     }
     if(!MailPause->Checked) {
-        while(GetServerLogLine(mail_log,NTSVC_NAME_MAIL,&msg))
-            mail_log_msg(&msg);
+		count = 0;
+        while(count < max_lines && GetServerLogLine(mail_log,NTSVC_NAME_MAIL,&msg))
+            mail_log_msg(&msg), count++;
 
-        while((pmsg=(log_msg_t*)listShiftNode(&mail_log_list)) != NULL) {
+        while(count < max_lines && (pmsg=(log_msg_t*)listShiftNode(&mail_log_list)) != NULL) {
             mail_log_msg(pmsg);
             free(pmsg);
+			count++;
         }
+		if(count)
+			logged_msgs(MailForm->Log);
     }
     if(!WebPause->Checked) {
-        while(GetServerLogLine(web_log,NTSVC_NAME_WEB,&msg))
-            web_log_msg(&msg);
+		count = 0;
+        while(count < max_lines && GetServerLogLine(web_log,NTSVC_NAME_WEB,&msg))
+            web_log_msg(&msg), count++;
 
-        while((pmsg=(log_msg_t*)listShiftNode(&web_log_list)) != NULL) {
+        while(count < max_lines && (pmsg=(log_msg_t*)listShiftNode(&web_log_list)) != NULL) {
             web_log_msg(pmsg);
             free(pmsg);
+			count++;
         }
+		if(count)
+			logged_msgs(WebForm->Log);
     }
     if(!ServicesPause->Checked) {
-        while(GetServerLogLine(services_log,NTSVC_NAME_SERVICES,&msg))
-            services_log_msg(&msg);
+		count = 0;
+        while(count < max_lines && GetServerLogLine(services_log,NTSVC_NAME_SERVICES,&msg))
+            services_log_msg(&msg), count++;
 
-        while((pmsg=(log_msg_t*)listShiftNode(&services_log_list)) != NULL) {
+        while(count < max_lines && (pmsg=(log_msg_t*)listShiftNode(&services_log_list)) != NULL) {
             services_log_msg(pmsg);
             free(pmsg);
+			count++;
         }
+		if(count)
+			logged_msgs(ServicesForm->Log);
     }
 }
 //---------------------------------------------------------------------------
diff --git a/src/sbbs3/ctrl/sbbsctrl.bpr b/src/sbbs3/ctrl/sbbsctrl.bpr
index 05f779670b31dbe298f5355cf84523695cdfef04..46803f62b9aa327027331608323531496140dbd8 100644
--- a/src/sbbs3/ctrl/sbbsctrl.bpr
+++ b/src/sbbs3/ctrl/sbbsctrl.bpr
@@ -48,7 +48,7 @@
     <DEBUGLIBPATH value="$(BCB)\lib\debug"/>
     <RELEASELIBPATH value="$(BCB)\lib\release"/>
     <LINKER value="ilink32"/>
-    <USERDEFINES value="SBBS;RINGBUF_SEM;RINGBUF_MUTEX;USE_CRYPTLIB;_DEBUG"/>
+    <USERDEFINES value="SBBS;RINGBUF_SEM;RINGBUF_MUTEX;RINGBUF_EVENT;USE_CRYPTLIB;_DEBUG"/>
     <SYSDEFINES value="NO_STRICT;_VIS_NOLIB"/>
     <MAINSOURCE value="sbbsctrl.cpp"/>
     <INCLUDEPATH value="..\;..;$(BCB)\Projects;..\..\xpdev;..\..\smblib;..\..\hash;$(BCB)\include;$(BCB)\include\vcl;..\..\..\3rdp\win32.release\cryptlib\include;..\..\comio"/>
diff --git a/src/sbbs3/dat_rec.c b/src/sbbs3/dat_rec.c
index c9859704282f2257cfe33f96c71168394d801cdc..8b401a94cb6052063a5b6703cbe432a52028bb9c 100644
--- a/src/sbbs3/dat_rec.c
+++ b/src/sbbs3/dat_rec.c
@@ -27,7 +27,7 @@
 /* Places into 'strout' CR or ETX terminated string starting at             */
 /* 'start' and ending at 'start'+'length' or terminator from 'strin'        */
 /****************************************************************************/
-int DLLCALL getrec(const char *strin,int start,int length,char *strout)
+int getrec(const char *strin,int start,int length,char *strout)
 {
     int i=0,stop;
 
@@ -45,7 +45,7 @@ int DLLCALL getrec(const char *strin,int start,int length,char *strout)
 /* Places into 'strout', 'strin' starting at 'start' and ending at          */
 /* 'start'+'length'                                                         */
 /****************************************************************************/
-void DLLCALL putrec(char *strout,int start,int length,char *strin)
+void putrec(char *strout,int start,int length, const char *strin)
 {
     int i=0,j;
 
diff --git a/src/sbbs3/dat_rec.h b/src/sbbs3/dat_rec.h
index b266c0bd049ba9e7bcf5c863b9303218d4b80a72..032ae9fde4c27c3143fe413f06df37d0657cf07a 100644
--- a/src/sbbs3/dat_rec.h
+++ b/src/sbbs3/dat_rec.h
@@ -1,9 +1,5 @@
-/* dat_rec.h */
-
 /* Synchronet text data access routines (exported) */
 
-/* $Id: dat_rec.h,v 1.5 2019/03/22 21:28:27 rswindell Exp $ */
-
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
  * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
@@ -17,61 +13,23 @@
  * See the GNU General Public License for more details: gpl.txt or			*
  * http://www.fsf.org/copyleft/gpl.html										*
  *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
- *																			*
  * For Synchronet coding style and modification guidelines, see				*
  * http://www.synchro.net/source.html										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
 #ifndef _DAT_REC_H
 #define _DAT_REC_H
 
-#ifdef DLLEXPORT
-#undef DLLEXPORT
-#endif
-#ifdef DLLCALL
-#undef DLLCALL
-#endif
-
-#ifdef _WIN32
-	#ifdef __MINGW32__
-		#define DLLEXPORT
-		#define DLLCALL
-	#else
-		#ifdef SBBS_EXPORTS
-			#define DLLEXPORT __declspec(dllexport)
-		#else
-			#define DLLEXPORT __declspec(dllimport)
-		#endif
-		#ifdef __BORLANDC__
-			#define DLLCALL
-		#else
-			#define DLLCALL
-		#endif
-	#endif
-#else
-	#define DLLEXPORT
-	#define DLLCALL
-#endif
+#include "dllexport.h"
 
 #ifdef __cplusplus
 extern "C" {
 #endif
 
-DLLEXPORT int	DLLCALL getrec(const char *instr,int start,int length,char *outstr); /* Retrieve a record from a string */
-DLLEXPORT void	DLLCALL putrec(char *outstr,int start,int length,char *instr); /* Place a record into a string */
+DLLEXPORT int	getrec(const char *instr, int start, int length, char *outstr); /* Retrieve a record from a string */
+DLLEXPORT void	putrec(char *outstr, int start, int length, const char *instr); /* Place a record into a string */
 
 #ifdef __cplusplus
 }
diff --git a/src/sbbs3/data.cpp b/src/sbbs3/data.cpp
index cbc899fb12f7c47d0ecf7e5722f854ad2008b2e2..59ad589622522d72946c502f757b5bb6b3962411 100644
--- a/src/sbbs3/data.cpp
+++ b/src/sbbs3/data.cpp
@@ -97,42 +97,6 @@ uint sbbs_t::finduser(const char* instr, bool silent_failure)
 	return(0);
 }
 
-/****************************************************************************/
-/* Returns the number of user transfers in XFER.IXT for either a dest user  */
-/* source user, or filename.												*/
-/****************************************************************************/
-int sbbs_t::getuserxfers(int fromuser, int destuser, char *fname)
-{
-	char str[256];
-	int file,found=0;
-	FILE *stream;
-
-	SAFEPRINTF(str,"%sxfer.ixt",cfg.data_dir);
-	if(!fexist(str))
-		return(0);
-	if(!flength(str)) {
-		remove(str);
-		return(0); 
-	}
-	if((stream=fnopen(&file,str,O_RDONLY))==NULL) {
-		errormsg(WHERE,ERR_OPEN,str,O_RDONLY);
-		return(0); 
-	}
-	while(!ferror(stream)) {
-		if(!fgets(str,81,stream))
-			break;
-		str[22]=0;
-		if(fname!=NULL && fname[0] && !strncmp(str+5,fname,12))
-				found++;
-		else if(fromuser && atoi(str+18)==fromuser)
-				found++;
-		else if(destuser && atoi(str)==destuser)
-				found++; 
-	}
-	fclose(stream);
-	return(found);
-}
-
 /****************************************************************************/
 /* Return date/time that the specified event should run next				*/
 /****************************************************************************/
diff --git a/src/sbbs3/delfiles.c b/src/sbbs3/delfiles.c
index a017ff8b2bfbccefccab6493aacf8b75bf3e4f4f..3104f93db2e3a46f7b14e1f2e42e9306f7686205 100644
--- a/src/sbbs3/delfiles.c
+++ b/src/sbbs3/delfiles.c
@@ -23,10 +23,12 @@
 #include "load_cfg.h"
 #include "filedat.h"
 #include "nopen.h"
+#include "smblib.h"
 #include "str_util.h"
 #include <stdarg.h>
+#include <stdbool.h>
 
-#define DELFILES_VER "1.01"
+#define DELFILES_VER "3.19"
 
 char tmp[256];
 char *crlf="\r\n";
@@ -75,33 +77,45 @@ int lprintf(const char *fmat, ...)
 	return(chcount);
 }
 
+bool delfile(const char *filename)
+{
+	if(remove(filename) != 0) {
+		fprintf(stderr, "ERROR %u (%s) removing file %s\n"
+			,errno, strerror(errno), filename);
+		return false;
+	}
+	return true;
+}
+
 int main(int argc, char **argv)
 {
-	char str[256],fname[MAX_PATH+1],not[MAX_NOTS][9],nots=0,*p;
-	int i,j,dirnum,libnum,file;
-	ulong l,m;
+	char revision[16];
+	char str[256],not[MAX_NOTS][LEN_EXTCODE + 1],nots=0,*p;
+	char fpath[MAX_PATH+1];
+	int i,j,dirnum,libnum;
+	size_t fi;
 	long misc=0;
-	time_t now;
-	file_t workfile;
+	time_t now = time(NULL);
 	scfg_t cfg;
 	glob_t gl;
-	uchar *ixbbuf;
 
 	setvbuf(stdout,NULL,_IONBF,0);
 
-	fprintf(stderr,"\nDELFILES Version %s (%s) - Removes files from Synchronet "
-		"Filebase\n" ,DELFILES_VER, PLATFORM_DESC );
+	sscanf("$Revision: 1.54 $", "%*s %s", revision);
+
+	fprintf(stderr,"\nDELFILES Version %s-%s (rev %s) - Removes files from Synchronet "
+		"Filebase\n" ,DELFILES_VER, PLATFORM_DESC, revision);
 
 	if(argc<2) {
-		printf("\n   usage: DELFILES [dir_code] [switches]\n");
-		printf("\nswitches: -LIB name All directories of specified library\n");
-		printf("          -ALL      Search all directories\n");
-		printf("          -NOT code Exclude specific directory\n");
-		printf("          -OFF      Remove files that are offline "
+		printf("\n   usage: %s <dir_code or * for ALL> [switches]\n", argv[0]);
+		printf("\nswitches: -lib name All directories of specified library\n");
+		printf("          -all      Search all directories\n");
+		printf("          -not code Exclude specific directory\n");
+		printf("          -off      Remove files that are offline "
 			"(don't exist on disk)\n");
-		printf("          -NOL      Remove files with no link "
+		printf("          -nol      Remove files with no link "
 			"(don't exist in database)\n");
-		printf("          -RPT      Report findings only "
+		printf("          -rpt      Report findings only "
 			"(don't delete any files)\n");
 		return(0); 
 	}
@@ -162,7 +176,8 @@ int main(int argc, char **argv)
 				printf("\nDirectory internal code must follow /NOT parameter.\n");
 				return(1); 
 			}
-			sprintf(not[nots++],"%.8s",argv[i]); 
+			SAFECOPY(not[nots], argv[i]); 
+			nots++;
 		}
 		else if(!stricmp(argv[i]+1,"OFF"))
 			misc|=OFFLINE;
@@ -187,117 +202,88 @@ int main(int argc, char **argv)
 		if(!(misc&ALL) && i!=dirnum && cfg.dir[i]->lib!=libnum)
 			continue;
 		for(j=0;j<nots;j++)
-			if(!stricmp(not[j],cfg.dir[i]->code))
+			if(!stricmp(not[j], cfg.dir[i]->code))
 				break;
 		if(j<nots)
 			continue;
 
+		smb_t smb;
+		int result = smb_open_dir(&cfg, &smb, i);
+		if(result != SMB_SUCCESS) {
+			fprintf(stderr, "!ERROR %d (%s) opening %s\n", result, smb.last_error, smb.file);
+			continue;
+		}
+
 		if(misc&NO_LINK && cfg.dir[i]->misc&DIR_FCHK) {
-			strcpy(tmp,cfg.dir[i]->path);
-			SAFEPRINTF(str,"%s*.*",tmp);
-			printf("\nSearching %s for unlinked files\n",str);
-			if(!glob(str, GLOB_MARK, NULL, &gl)) {
+			printf("\nSearching %s for unlinked files\n", cfg.dir[i]->path);
+			sprintf(str, "%s%s", cfg.dir[i]->path, ALLFILES);
+			if(glob(str, GLOB_MARK, NULL, &gl) == 0) {
 				for(j=0; j<(int)gl.gl_pathc; j++) {
+					const char* fname = gl.gl_pathv[j];
 					/* emulate _A_NORMAL */
-					if(isdir(gl.gl_pathv[j]))
+					if(isdir(fname))
 						continue;
-					if(access(gl.gl_pathv[j], R_OK|W_OK))
+					if(access(fname, R_OK|W_OK))
 						continue;
-					padfname(gl.gl_pathv[j],str);
-					/* strupr(str); */
-					if(!findfile(&cfg, i,str)) {
-						sprintf(str,"%s%s",tmp,gl.gl_pathv[j]);
-						printf("Removing %s (not in database)\n",gl.gl_pathv[j]);
-						if(!(misc&REPORT) && remove(str))
-							printf("Error removing %s\n",str); 
-					} 
-				} 
+					if(!smb_findfile(&smb, getfname(fname), /* file: */NULL)) {
+						printf("Not in database: %s\n", fname);
+						if(!(misc&REPORT))
+							delfile(fname);
+					}
+				}
 			}
 			globfree(&gl);
 		}
 
-		if(!cfg.dir[i]->maxage && !(misc&OFFLINE))
+		if(!cfg.dir[i]->maxage && !(misc&OFFLINE)) {
+			smb_close(&smb);
 			continue;
+		}
 
-		printf("\nScanning %s %s\n",cfg.lib[cfg.dir[i]->lib]->sname,cfg.dir[i]->lname);
+		printf("\nScanning %s %s\n", cfg.lib[cfg.dir[i]->lib]->sname, cfg.dir[i]->lname);
 
-		sprintf(str,"%s%s.ixb",cfg.dir[i]->data_dir,cfg.dir[i]->code);
-		if((file=nopen(str,O_RDONLY|O_BINARY))==-1)
-			continue;
-		l=filelength(file);
-		if(!l) {
-			close(file);
-			continue; 
-		}
-		if((ixbbuf=malloc(l))==NULL) {
-			close(file);
-			printf("\7ERR_ALLOC %s %lu\n",str,l);
-			continue; 
-		}
-		if(read(file,ixbbuf,l)!=(int)l) {
-			close(file);
-			printf("\7ERR_READ %s %lu\n",str,l);
-			free((char *)ixbbuf);
-			continue; 
-		}
-		close(file);
+		size_t file_count;
+		file_t* file_list = loadfiles(&smb, NULL, 0, /* extdesc: */FALSE, FILE_SORT_NATURAL, &file_count);
 
-		m=0L;
-		now=time(NULL);
-		while(m<l) {
-			memset(&workfile,0,sizeof(file_t));
-			for(j=0;j<12 && m<l;j++)
-				if(j==8)
-					fname[j]='.';
-				else
-					fname[j]=ixbbuf[m++];
-			fname[j]=0;
-			strcpy(workfile.name,fname);
-			unpadfname(workfile.name,fname);
-			workfile.dir=i;
-			SAFEPRINTF2(str,"%s%s"
-				,workfile.altpath>0 && workfile.altpath<=cfg.altpaths
-					? cfg.altpath[workfile.altpath-1]
-				: cfg.dir[workfile.dir]->path,fname);
-			workfile.datoffset=ixbbuf[m]|((long)ixbbuf[m+1]<<8)
-				|((long)ixbbuf[m+2]<<16);
-			workfile.dateuled=(ixbbuf[m+3]|((long)ixbbuf[m+4]<<8)
-				|((long)ixbbuf[m+5]<<16)|((long)ixbbuf[m+6]<<24));
-			workfile.datedled=(ixbbuf[m+7]|((long)ixbbuf[m+8]<<8)
-				|((long)ixbbuf[m+9]<<16)|((long)ixbbuf[m+10]<<24));
-			m+=11;
-			if(cfg.dir[i]->maxage && cfg.dir[i]->misc&DIR_SINCEDL && workfile.datedled
-				&& (now-workfile.datedled)/86400L>cfg.dir[i]->maxage) {
-					printf("Deleting %s (%ld days since last download)\n",fname
-						,(long)(now-workfile.datedled)/86400L);
-					getfiledat(&cfg, &workfile);
+		for(fi = 0; fi < file_count; fi++) {
+			file_t* f = &file_list[fi];
+			getfilepath(&cfg, f, fpath);
+			if(cfg.dir[i]->maxage && cfg.dir[i]->misc&DIR_SINCEDL && f->hdr.last_downloaded
+				&& (now - f->hdr.last_downloaded)/86400L > cfg.dir[i]->maxage) {
+					printf("Deleting %s (%ld days since last download)\n"
+						,f->name
+						,(long)(now - f->hdr.last_downloaded)/86400L);
 					if(!(misc&REPORT)) {
-						removefiledat(&cfg, &workfile);
-						if(remove(str))
-							printf("Error removing %s\n",str); 
+						if(smb_removefile(&smb, f) == SMB_SUCCESS)
+							delfile(fpath);
+						else
+							printf("ERROR (%s) removing file from database\n", smb.last_error);
 					} 
 			}
 			else if(cfg.dir[i]->maxage
-				&& !(workfile.datedled && cfg.dir[i]->misc&DIR_SINCEDL)
-				&& (now-workfile.dateuled)/86400L>cfg.dir[i]->maxage) {
-					printf("Deleting %s (uploaded %ld days ago)\n",fname
-						,(long)(now-workfile.dateuled)/86400L);
-					getfiledat(&cfg, &workfile);
+				&& !(f->hdr.last_downloaded && cfg.dir[i]->misc&DIR_SINCEDL)
+				&& (now - f->hdr.when_imported.time)/86400L > cfg.dir[i]->maxage) {
+					printf("Deleting %s (uploaded %ld days ago)\n"
+						,f->name
+						,(long)(now - f->hdr.when_imported.time)/86400L);
 					if(!(misc&REPORT)) {
-						removefiledat(&cfg, &workfile);
-						if(remove(str))
-							printf("Error removing %s\n",str); 
+						if(smb_removefile(&smb, f) == SMB_SUCCESS)
+							delfile(fpath);
+						else
+							printf("ERROR (%s) removing file from database\n", smb.last_error);
 					} 
 			}
-			else if(misc&OFFLINE && cfg.dir[i]->misc&DIR_FCHK && !fexist(str)) {
-					printf("Removing %s (doesn't exist)\n",fname);
-					getfiledat(&cfg, &workfile);
-					if(!(misc&REPORT))
-						removefiledat(&cfg, &workfile); 
+			else if(misc&OFFLINE && cfg.dir[i]->misc&DIR_FCHK && !fexist(fpath)) {
+					printf("Removing %s (doesn't exist)\n", f->name);
+					if(!(misc&REPORT)) {
+						if(smb_removefile(&smb, f) != SMB_SUCCESS)
+							printf("ERROR (%s) removing file from database\n", smb.last_error);
+					}
 			} 
 		}
+		freefiles(file_list, file_count);
 
-		free((char *)ixbbuf); 
+		smb_close(&smb);
 	}
 
 	return(0);
diff --git a/src/sbbs3/delfiles.vcxproj b/src/sbbs3/delfiles.vcxproj
index 5b201b9f583488271cd2ce32c873a42d5ac27f09..af7f406281f85fe6d4cb715c2301ea6a0d5482b2 100644
--- a/src/sbbs3/delfiles.vcxproj
+++ b/src/sbbs3/delfiles.vcxproj
@@ -38,6 +38,7 @@
     <Import Project="..\build\target_ia32.props" />
     <Import Project="..\hash\hash.props" />
     <Import Project="..\encode\encode.props" />
+    <Import Project="..\..\3rdp\win32.release\libarchive\libarchive.props" />
   </ImportGroup>
   <ImportGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="PropertySheets">
     <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
@@ -48,6 +49,7 @@
     <Import Project="..\build\target_ia32.props" />
     <Import Project="..\hash\hash.props" />
     <Import Project="..\encode\encode.props" />
+    <Import Project="..\..\3rdp\win32.release\libarchive\libarchive.props" />
   </ImportGroup>
   <PropertyGroup Label="UserMacros" />
   <PropertyGroup>
@@ -160,8 +162,12 @@
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
     </ClCompile>
+    <ClCompile Include="userdat.c" />
   </ItemGroup>
   <ItemGroup>
+    <ProjectReference Include="..\smblib\smblib.vcxproj">
+      <Project>{d674842b-2f41-42cb-9426-b3c4b0682574}</Project>
+    </ProjectReference>
     <ProjectReference Include="..\xpdev\xpdev.vcxproj">
       <Project>{7428a1e8-56b7-4868-9c0e-29d031689feb}</Project>
       <ReferenceOutputAssembly>false</ReferenceOutputAssembly>
diff --git a/src/sbbs3/download.cpp b/src/sbbs3/download.cpp
index e4dad09a542f8788f45387d945ef6630a70b9426..8590bba83f80d20dacfbd4ffb0b3cbf37f89025c 100644
--- a/src/sbbs3/download.cpp
+++ b/src/sbbs3/download.cpp
@@ -21,129 +21,46 @@
 
 #include "sbbs.h"
 #include "telnet.h"
+#include "filedat.h"
 
 /****************************************************************************/
+/* Call this function *AFTER* a file has been successfully downloaded		*/
 /* Updates downloader, uploader and downloaded file data                    */
 /* Must have offset, dir and name fields filled prior to call.              */
 /****************************************************************************/
-void sbbs_t::downloadfile(file_t* f)
+void sbbs_t::downloadedfile(file_t* f)
 {
-    char		str[MAX_PATH+1],fname[13];
+    char		str[MAX_PATH+1];
 	char 		tmp[512];
-    int			i,file;
-	long		mod;
-	long		length;
-    ulong		l;
-	user_t		uploader;
-
-	getfiledat(&cfg,f); /* Get current data - right after download */
-	if((length=f->size)<0L)
-		length=0L;
-	if(!(cfg.dir[f->dir]->misc&DIR_NOSTAT)) {
-		logon_dlb+=length;  /* Update 'this call' stats */
+	off_t		length;
+
+	length = getfilesize(&cfg, f);
+	if(length > 0 && !(cfg.dir[f->dir]->misc&DIR_NOSTAT)) {
+		logon_dlb += length;  /* Update 'this call' stats */
 		logon_dls++;
 	}
-	bprintf(text[FileNBytesSent],f->name,ultoac(length,tmp));
+	bprintf(text[FileNBytesSent],f->name,ultoac((ulong)length,tmp));
 	SAFEPRINTF3(str,"downloaded %s from %s %s"
 		,f->name,cfg.lib[cfg.dir[f->dir]->lib]->sname
 		,cfg.dir[f->dir]->sname);
 	logline("D-",str);
-	/****************************/
-	/* Update Downloader's Info */
-	/****************************/
-	user_downloaded(&cfg, &useron, 1, length);
-	if(!is_download_free(&cfg,f->dir,&useron,&client))
-		subtract_cdt(&cfg,&useron,f->cdt);
-	/**************************/
-	/* Update Uploader's Info */
-	/**************************/
-	i=matchuser(&cfg,f->uler,TRUE /*sysop_alias*/);
-	memset(&uploader, 0, sizeof(uploader));
-	uploader.number=i;
-	getuserdat(&cfg,&uploader);
-	if(i && i!=useron.number && uploader.firston<f->dateuled) {
-		l=f->cdt;
-		if(!(cfg.dir[f->dir]->misc&DIR_CDTDL))	/* Don't give credits on d/l */
-			l=0;
-		if(cfg.dir[f->dir]->misc&DIR_CDTMIN && cur_cps) { /* Give min instead of cdt */
-			mod=((ulong)(l*(cfg.dir[f->dir]->dn_pct/100.0))/cur_cps)/60;
-			adjustuserrec(&cfg,i,U_MIN,10,mod);
-			SAFEPRINTF(tmp,"%lu minute",mod);
-		} else {
-			mod=(ulong)(l*(cfg.dir[f->dir]->dn_pct/100.0));
-			adjustuserrec(&cfg,i,U_CDT,10,mod);
-			ultoac(mod,tmp);
-		}
-		if(!(cfg.dir[f->dir]->misc&DIR_QUIET)) {
-			SAFEPRINTF4(str,text[DownloadUserMsg]
-				,!strcmp(cfg.dir[f->dir]->code,"TEMP") ? temp_file : f->name
-				,!strcmp(cfg.dir[f->dir]->code,"TEMP") ? text[Partially] : nulstr
-				,useron.alias,tmp);
-			putsmsg(&cfg,i,str);
-		}
-	}
-	/*******************/
-	/* Update IXB File */
-	/*******************/
-	f->datedled=time32(NULL);
-	SAFEPRINTF2(str,"%s%s.ixb",cfg.dir[f->dir]->data_dir,cfg.dir[f->dir]->code);
-	if((file=nopen(str,O_RDWR))==-1) {
-		errormsg(WHERE,ERR_OPEN,str,O_RDWR);
-		return;
-	}
-	length=(long)filelength(file);
-	if(length%F_IXBSIZE) {
-		close(file);
-		errormsg(WHERE,ERR_LEN,str,length);
-		return;
-	}
-	strcpy(fname,f->name);
-	for(i=8;i<12;i++)   /* Turn FILENAME.EXT into FILENAMEEXT */
-		fname[i]=fname[i+1];
-	for(l=0;l<(ulong)length;l+=F_IXBSIZE) {
-		read(file,str,F_IXBSIZE);      /* Look for the filename in the IXB file */
-		str[11]=0;
-		if(!stricmp(fname,str))
-			break;
-	}
-	if(l>=(ulong)length) {
-		close(file);
-		errormsg(WHERE,ERR_CHK,f->name,0);
-		return;
-	}
-	lseek(file,l+18,SEEK_SET);
-	write(file,&f->datedled,4);  /* Write the current time stamp for datedled */
-	close(file);
-	/*******************/
-	/* Update DAT File */
-	/*******************/
-	f->timesdled++;
-	putfiledat(&cfg,f);
-	/******************************************/
-	/* Update User to User index if necessary */
-	/******************************************/
-	if(f->dir==cfg.user_dir) {
-		rmuserxfers(&cfg,0,useron.number,f->name);
-		if(!getuserxfers(0,0,f->name)) { /* check if any ixt entries left */
-			remove(getfilepath(&cfg,f,str));
-			removefiledat(&cfg,f);
-		}
-	}
+
+	user_downloaded_file(&cfg, &useron, &client, f->dir, f->name, length);
 
 	user_event(EVENT_DOWNLOAD);
 }
 
 /****************************************************************************/
 /* This function is called when a file is unsuccessfully downloaded.        */
-/* It logs the tranfer time and checks for possible leech protocol use.     */
+/* It logs the transfer time and checks for possible leech protocol use.    */
 /****************************************************************************/
-void sbbs_t::notdownloaded(ulong size, time_t start, time_t end)
+void sbbs_t::notdownloaded(off_t size, time_t start, time_t end)
 {
     char	str[256],tmp2[256];
 	char 	tmp[512];
 
 	SAFEPRINTF2(str,"Estimated Time: %s  Transfer Time: %s"
-		,sectostr(cur_cps ? size/cur_cps : 0,tmp)
+		,sectostr(cur_cps ? (uint)(size/cur_cps) : 0,tmp)
 		,sectostr((uint)(end-start),tmp2));
 	logline(nulstr,str);
 	if(cfg.leech_pct && cur_cps                 /* leech detection */
@@ -166,8 +83,6 @@ const char* sbbs_t::protcmdline(prot_t* prot, enum XFER_TYPE type)
 			return(prot->batulcmd);
 		case XFER_BATCH_DOWNLOAD:
 			return(prot->batdlcmd);
-		case XFER_BIDIR:
-			return(prot->bicmd);
 	}
 
 	return("invalid transfer type");
@@ -177,7 +92,7 @@ const char* sbbs_t::protcmdline(prot_t* prot, enum XFER_TYPE type)
 /* Handles start and stop routines for transfer protocols                   */
 /****************************************************************************/
 int sbbs_t::protocol(prot_t* prot, enum XFER_TYPE type
-					 ,char *fpath, char *fspec, bool cd, bool autohangup)
+					 ,const char *fpath, const char *fspec, bool cd, bool autohangup)
 {
 	char	protlog[256],*p;
 	char*	cmdline;
@@ -226,8 +141,6 @@ int sbbs_t::protocol(prot_t* prot, enum XFER_TYPE type
 	request_telnet_opt(TELNET_WONT,TELNET_BINARY_TX);
 
 	sys_status&=~SS_FILEXFER;
-	if(online==ON_REMOTE)
-		rioctl(IOFB);
 
 	// Save DSZLOG to logfile
 	if((stream=fnopen(NULL,protlog,O_RDONLY))!=NULL) {
@@ -306,7 +219,7 @@ bool sbbs_t::checkdszlog(const char* fpath)
 
 	SAFEPRINTF(path,"%sPROTOCOL.LOG",cfg.node_dir);
 	if((fp=fopen(path,"r"))==NULL)
-		return(false);
+		return false;
 
 	SAFECOPY(rpath, fpath);
 	fexistcase(rpath);
@@ -355,40 +268,50 @@ bool sbbs_t::checkdszlog(const char* fpath)
 
 /****************************************************************************/
 /* Checks dsz compatible log file for errors in transfer                    */
-/* Returns 1 if the file in the struct file_t was successfuly transfered    */
+/* Returns 1 if the file in the struct file_t was successfully transfered   */
 /****************************************************************************/
-bool sbbs_t::checkprotresult(prot_t* prot, int error, file_t* f)
+bool sbbs_t::checkprotresult(prot_t* prot, int error, const char* fpath)
 {
-	char str[512];
-	char tmp[128];
 	bool success;
-	char fpath[MAX_PATH+1];
 
-	getfilepath(&cfg,f,fpath);
 	if(prot->misc&PROT_DSZLOG)
 		success=checkdszlog(fpath);
 	else
 		success=(error==0);
 
-	if(!success) {
-		bprintf(text[FileNotSent],f->name);
+	if(!success)
+		bprintf(text[FileNotSent], getfname(fpath));
+
+	return success;
+}
+
+bool sbbs_t::checkprotresult(prot_t* prot, int error, file_t* f)
+{
+	char str[512];
+	char tmp[128];
+	char fpath[MAX_PATH+1];
+
+	getfilepath(&cfg, f, fpath);
+	if(!checkprotresult(prot, error, fpath)) {
 		if(f->dir<cfg.total_dirs)
 			SAFEPRINTF4(str,"attempted to download %s (%s) from %s %s"
-				,f->name,ultoac(f->size,tmp)
+				,f->name,ultoac((ulong)f->size,tmp)
 				,cfg.lib[cfg.dir[f->dir]->lib]->sname,cfg.dir[f->dir]->sname);
 		else if(f->dir==cfg.total_dirs)
 			SAFECOPY(str,"attempted to download QWK packet");
-		else if(f->dir==cfg.total_dirs+1)
+		else if(f->dir == cfg.total_dirs + 1)
 			SAFEPRINTF(str,"attempted to download attached file: %s"
 				,f->name);
+		else
+			SAFEPRINTF2(str,"attempted to download file (%s) from unknown dir: %ld"
+				,f->name, (long)f->dir);
 		logline(LOG_NOTICE,"D!",str);
-		return(false);
+		return false; 
 	}
-	return(true);
+	return true;
 }
 
 
-
 /************************************************************************/
 /* Wait (for a limited period of time) for sequential dev to become 	*/
 /* available for file retrieval 										*/
@@ -453,7 +376,7 @@ bool sbbs_t::sendfile(char* fname, char prot, const char* desc, bool autohang)
 		ch=(char)getkeys(keys,0);
 
 		if(ch==text[YNQP][2] || sys_status&SS_ABORT)
-			return(false);
+			return false; 
 	}
 	for(i=0;i<cfg.total_prots;i++)
 		if(cfg.prot[i]->mnemonic==ch && chk_ar(cfg.prot[i]->ar,&useron,&client))
@@ -471,9 +394,9 @@ bool sbbs_t::sendfile(char* fname, char prot, const char* desc, bool autohang)
 		logon_dlb += length;	/* Update stats */
 		logon_dls++;
 		useron.dls = (ushort)adjustuserrec(&cfg, useron.number, U_DLS, 5, 1);
-		useron.dlb = adjustuserrec(&cfg,useron.number, U_DLB, 10, length);
+		useron.dlb = adjustuserrec(&cfg,useron.number, U_DLB, 10, (long)length);
 		char bytes[32];
-		ultoac(length, bytes);
+		ultoac((ulong)length, bytes);
 		bprintf(text[FileNBytesSent], getfname(fname), bytes);
 		char str[128];
 		SAFEPRINTF3(str, "downloaded %s: %s (%s bytes)"
@@ -488,3 +411,27 @@ bool sbbs_t::sendfile(char* fname, char prot, const char* desc, bool autohang)
 	}
 	return result;
 }
+
+// contains some copy/pasta from downloadedfile()
+bool sbbs_t::sendfile(file_t* f, char prot, bool autohang)
+{
+	char path[MAX_PATH + 1];
+	char str[256];
+
+	SAFEPRINTF2(str, "from %s %s"
+		,cfg.lib[cfg.dir[f->dir]->lib]->sname
+		,cfg.dir[f->dir]->sname);
+	bool result = sendfile(getfilepath(&cfg, f, path), prot, str, autohang);
+	if(result == true) {
+		if(cfg.dir[f->dir]->misc&DIR_TFREE && cur_cps)
+			starttime += f->size / (ulong)cur_cps;
+		off_t length = getfilesize(&cfg, f);
+		if(length > 0 && !(cfg.dir[f->dir]->misc&DIR_NOSTAT)) {
+			logon_dlb += length;  /* Update 'this call' stats */
+			logon_dls++;
+		}
+		user_downloaded_file(&cfg, &useron, &client, f->dir, f->name, length);
+		user_event(EVENT_DOWNLOAD);
+	}
+	return result;
+}
diff --git a/src/sbbs3/dupefind.c b/src/sbbs3/dupefind.c
index b2ec2b19909c224dd7715626813c735defc53499..f8d514e3ab790625fef31a4148fda2e05099d9c6 100644
--- a/src/sbbs3/dupefind.c
+++ b/src/sbbs3/dupefind.c
@@ -20,11 +20,12 @@
 #include "scfgdefs.h"
 #include "str_util.h"
 #include "load_cfg.h"
+#include "smblib.h"
 #include "nopen.h"
 #include "crc32.h"
 #include <stdarg.h>
 
-#define DUPEFIND_VER "1.02"
+#define DUPEFIND_VER "3.19"
 
 char* crlf="\r\n";
 
@@ -65,37 +66,38 @@ int lprintf(const char *fmat, ...)
 	return(chcount);
 }
 
-char *display_filename(scfg_t *cfg, ushort dir_num,ushort fil_off)
+char *display_filename(scfg_t *cfg, uint dirnum, uint32_t fil_off)
 {
 	static char str[256];
-	char fname[13];
-	int file;
-
-	sprintf(str,"%s%s.ixb",cfg->dir[dir_num]->data_dir,cfg->dir[dir_num]->code);
-	if((file=nopen(str,O_RDONLY))==-1)
-		return("UNKNOWN");
-	lseek(file,(long)(22*(fil_off-1)),SEEK_SET);
-	read(file,fname,11);
-	close(file);
-
-	sprintf(str,"%-8.8s.%c%c%c",fname,fname[8],fname[9],fname[10]);
-	return(str);
+	static smb_t smb;
+	if(smb_open_dir(cfg, &smb, dirnum) != SMB_SUCCESS)
+		return smb.last_error;
+	smb_fseek(smb.sid_fp, (fil_off - 1) * sizeof(fileidxrec_t), SEEK_SET);
+	fileidxrec_t idx;
+	if(smb_fread(&smb, &idx, sizeof(idx), smb.sid_fp) != sizeof(idx)) {
+		smb_close(&smb);
+		return smb.last_error;
+	}
+	smb_close(&smb);
+	SAFECOPY(str, idx.name);
+	return str;
 }
 
 int main(int argc,char **argv)
 {
-	char str[256],*ixbbuf,*p;
-	ulong **fcrc,*foundcrc,total_found=0L;
+	char str[256], *p;
+	uint32_t **fcrc,*foundcrc;
+	ulong total_found=0L;
 	ulong g;
-	ushort i,j,k,h,start_lib=0,end_lib=0,found=-1;
-	int file;
-    long l,m;
+	uint i,j,k,h,start_lib=0,end_lib=0,found=-1;
 	scfg_t cfg;
 
 	setvbuf(stdout,NULL,_IONBF,0);
 
-	fprintf(stderr,"\nDUPEFIND Version %s (%s) - Synchronet Duplicate File "
-		"Finder\n", DUPEFIND_VER, PLATFORM_DESC);
+	char revision[16];
+	sscanf("$Revision: 1.54 $", "%*s %s", revision);
+	fprintf(stderr,"\nDUPEFIND v%s-%s (rev %s) - Synchronet Duplicate File "
+		"Finder\n", DUPEFIND_VER, PLATFORM_DESC, revision);
 
     p = get_ctrl_dir(/* warn: */TRUE);
 
@@ -126,78 +128,73 @@ int main(int argc,char **argv)
 	if(argc>2)
         end_lib=atoi(argv[2])-1;
 
-	if((fcrc=(ulong **)malloc(cfg.total_dirs*sizeof(ulong *)))==NULL) {
+	if((fcrc = malloc(cfg.total_dirs*sizeof(uint32_t *)))==NULL) {
 		printf("Not enough memory for CRCs.\r\n");
 		return(1); 
 	}
+	memset(fcrc, 0, cfg.total_dirs*sizeof(uint32_t *));
 
 	for(i=0;i<cfg.total_dirs;i++) {
-		fprintf(stderr,"Reading directory index %u of %u\r",i+1,cfg.total_dirs);
-        sprintf(str,"%s%s.ixb",cfg.dir[i]->data_dir,cfg.dir[i]->code);
-		if((file=nopen(str,O_RDONLY|O_BINARY))==-1) {
-			fcrc[i]=(ulong *)malloc(1*sizeof(ulong));
-            fcrc[i][0]=0;
-			continue; 
+		if(cfg.dir[i]->lib < start_lib || cfg.dir[i]->lib > end_lib)
+			continue;
+		printf("Reading directory %u of %u\r",i+1,cfg.total_dirs);
+		smb_t smb;
+		int result = smb_open_dir(&cfg, &smb, i);
+		if(result != SMB_SUCCESS) {
+			fprintf(stderr, "!ERROR %d (%s) opening %s\n"
+				,result, smb.last_error, smb.file);
+			continue;
 		}
-        l=filelength(file);
-		if(!l || (cfg.dir[i]->lib<start_lib || cfg.dir[i]->lib>end_lib)) {
-            close(file);
-			fcrc[i]=(ulong *)malloc(1*sizeof(ulong));
-			fcrc[i][0]=0;
-            continue; 
+		if(smb.status.total_files < 1) {
+			smb_close(&smb);
+			continue;
 		}
-		if((fcrc[i]=(ulong *)malloc((l/22+2)*sizeof(ulong)))==NULL) {
+		if((fcrc[i] = malloc(smb.status.total_files * sizeof(uint32_t)))==NULL) {
             printf("Not enough memory for CRCs.\r\n");
             return(1); 
 		}
-		fcrc[i][0]=(ulong)(l/22);
-        if((ixbbuf=(char *)malloc(l))==NULL) {
-            close(file);
-            printf("\7Error allocating memory for index %s.\r\n",str);
-            continue; 
+		j=0;
+		fcrc[i][j++] = smb.status.total_files;
+		rewind(smb.sid_fp);
+		while(!feof(smb.sid_fp)) {
+			fileidxrec_t idx;
+			if(smb_fread(&smb, &idx, sizeof(idx), smb.sid_fp) != sizeof(idx))
+				break;
+			char filename[sizeof(idx.name) + 1];
+			SAFECOPY(filename, idx.name);
+			fcrc[i][j++]=crc32(filename, 0);
 		}
-        if(read(file,ixbbuf,l)!=l) {
-            close(file);
-            printf("\7Error reading %s.\r\n",str);
-			free(ixbbuf);
-            continue; 
-		}
-        close(file);
-		j=1;
-		m=0L;
-		while(m<l) {
-			sprintf(str,"%-11.11s",(ixbbuf+m));
-			strupr(str);
-			fcrc[i][j++]=crc32(str,0);
-			m+=22; 
-		}
-		free(ixbbuf); 
+		smb_close(&smb);
 	}
 	lputs("\n");
 
-	foundcrc=0L;
+	foundcrc = NULL;
 	for(i=0;i<cfg.total_dirs;i++) {
 		if(cfg.dir[i]->lib<start_lib || cfg.dir[i]->lib>end_lib)
 			continue;
 		lprintf("Scanning %s %s\n",cfg.lib[cfg.dir[i]->lib]->sname,cfg.dir[i]->sname);
+		if(fcrc[i] == NULL)
+			continue;
 		for(k=1;k<fcrc[i][0];k++) {
 			for(j=i+1;j<cfg.total_dirs;j++) {
 				if(cfg.dir[j]->lib<start_lib || cfg.dir[j]->lib>end_lib)
 					continue;
+				if(fcrc[j] == NULL)
+					continue;
 				for(h=1;h<fcrc[j][0];h++) {
 					if(fcrc[i][k]==fcrc[j][h]) {
 						if(found!=k) {
 							found=k;
 							for(g=0;g<total_found;g++) {
 								if(foundcrc[g]==fcrc[i][k])
-									g=total_found+1; 
+									break; 
 							}
 							if(g==total_found) {
 								++total_found;
-								if((foundcrc=(ulong *)realloc(foundcrc
-									,total_found*sizeof(ulong)))==NULL) {
-								printf("Out of memory reallocating\r\n");
-								return(1); 
+								if((foundcrc = realloc(foundcrc
+									,total_found*sizeof(uint32_t)))==NULL) {
+									printf("Out of memory reallocating\r\n");
+									return(1); 
 								} 
 							}
 							else
diff --git a/src/sbbs3/dupefind.vcxproj b/src/sbbs3/dupefind.vcxproj
index daa7e3d2c0379b3950ee82964542e2c50cc40151..3a1846d25e69e73e070284d94f502cd71badfb38 100644
--- a/src/sbbs3/dupefind.vcxproj
+++ b/src/sbbs3/dupefind.vcxproj
@@ -38,6 +38,7 @@
     <Import Project="..\build\target_ia32.props" />
     <Import Project="..\hash\hash.props" />
     <Import Project="..\encode\encode.props" />
+    <Import Project="..\..\3rdp\win32.release\libarchive\libarchive.props" />
   </ImportGroup>
   <ImportGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="PropertySheets">
     <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
@@ -48,6 +49,7 @@
     <Import Project="..\build\target_ia32.props" />
     <Import Project="..\hash\hash.props" />
     <Import Project="..\encode\encode.props" />
+    <Import Project="..\..\3rdp\win32.release\libarchive\libarchive.props" />
   </ImportGroup>
   <PropertyGroup Label="UserMacros" />
   <PropertyGroup>
@@ -160,6 +162,7 @@
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
     </ClCompile>
+    <ClCompile Include="userdat.c" />
   </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\smblib\smblib.vcxproj">
diff --git a/src/sbbs3/email.cpp b/src/sbbs3/email.cpp
index 1a0245dc08e1ffb4644a22286eddc3324d63fb53..77cca6f921288dc0e43cbbd8ea08c27ea1e93747 100644
--- a/src/sbbs3/email.cpp
+++ b/src/sbbs3/email.cpp
@@ -40,7 +40,7 @@ bool sbbs_t::email(int usernumber, const char *top, const char *subj, long mode,
 	int 		i,j,x,file;
 	long		l;
 	long		length;
-	ulong		offset;
+	off_t		offset;
 	uint32_t	crc=0xffffffffUL;
 	FILE*		instream;
 	node_t		node;
@@ -271,7 +271,7 @@ bool sbbs_t::email(int usernumber, const char *top, const char *subj, long mode,
 		} 
 	}
 
-	msg.hdr.offset=offset;
+	msg.hdr.offset=(uint32_t)offset;
 
 	username(&cfg,usernumber,str);
 	smb_hfield_str(&msg,RECIPIENT,str);
diff --git a/src/sbbs3/exec.cpp b/src/sbbs3/exec.cpp
index 7900c368f3ce61892d06f1040bba01c5184a6dcc..cd9e0e5a5986145f72bd087507951620651fa247 100644
--- a/src/sbbs3/exec.cpp
+++ b/src/sbbs3/exec.cpp
@@ -1,4 +1,4 @@
-/* Synchronet command shell/module interpretter */
+/* Synchronet command shell/module interpreter */
 
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
@@ -802,7 +802,7 @@ long sbbs_t::exec_bin(const char *cmdline, csi_t *csi, const char* startup_dir)
 
 	memcpy(&bin,csi,sizeof(csi_t));
 	clearvars(&bin);
-	bin.length = filelength(file);
+	bin.length = (long)filelength(file);
 	if(bin.length < 1) {
 		close(file);
 		errormsg(WHERE, ERR_LEN, str, bin.length);
@@ -1157,7 +1157,8 @@ void sbbs_t::skipto(csi_t *csi, uchar inst)
 
 int sbbs_t::exec(csi_t *csi)
 {
-	char	str[256],*path;
+	char	str[256];
+	const char* path;
 	char 	tmp[512];
 	uchar	buf[1025],ch;
 	int 	i,j,file;
@@ -1227,7 +1228,7 @@ int sbbs_t::exec(csi_t *csi)
 							}
 							else if(text[i][0]==0) {
 								free(text[i]);
-								text[i]=nulstr; 
+								text[i]=(char*)nulstr; 
 							} 
 						}
 						if(i<TOTAL_TEXT) {
diff --git a/src/sbbs3/execfile.cpp b/src/sbbs3/execfile.cpp
index a27f220ea66a2db584613c5ba9e1755d598fdab4..dae7a9c871072f1274dfb52bfa3d10d5e85972e0 100644
--- a/src/sbbs3/execfile.cpp
+++ b/src/sbbs3/execfile.cpp
@@ -1,9 +1,5 @@
-/* execfile.cpp */
-
 /* Synchronet file transfer-related command shell/module routines */
 
-/* $Id: execfile.cpp,v 1.18 2020/05/24 08:11:45 rswindell Exp $ */
-
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
  * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
@@ -17,26 +13,15 @@
  * See the GNU General Public License for more details: gpl.txt or			*
  * http://www.fsf.org/copyleft/gpl.html										*
  *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
- *																			*
  * For Synchronet coding style and modification guidelines, see				*
  * http://www.synchro.net/source.html										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
 #include "sbbs.h"
 #include "cmdshell.h"
+#include "filedat.h"
 
 int sbbs_t::exec_file(csi_t *csi)
 {
@@ -319,43 +304,37 @@ int sbbs_t::exec_file(csi_t *csi)
 				bputs(text[R_Download]);
 				return(0); 
 			}
-			padfname(csi->str,str);
-			strupr(str);
-			if(!listfileinfo(usrdir[curlib][curdir[curlib]],str,FI_DOWNLOAD)) {
+			if(!listfileinfo(usrdir[curlib][curdir[curlib]], csi->str, FI_DOWNLOAD)
+				&& strcmp(csi->str, ALLFILES) != 0) {
 				bputs(text[SearchingAllDirs]);
-				for(i=0;i<usrdirs[curlib];i++)
+				for(i=0;i<usrdirs[curlib];i++) {
+					if(msgabort())
+						return 0;
 					if(i!=curdir[curlib] &&
-						(s=listfileinfo(usrdir[curlib][i],str,FI_DOWNLOAD))!=0)
-						if(s==-1 || (!strchr(str,'?') && !strchr(str,'*')))
+						(s=listfileinfo(usrdir[curlib][i],csi->str,FI_DOWNLOAD))!=0)
+						if(s==-1 || (!strchr(csi->str,'?') && !strchr(csi->str,'*')))
 							return(0);
+				}
 				bputs(text[SearchingAllLibs]);
 				for(i=0;i<usrlibs;i++) {
 					if(i==curlib) continue;
-					for(j=0;j<usrdirs[i];j++)
-						if((s=listfileinfo(usrdir[i][j],str,FI_DOWNLOAD))!=0)
-							if(s==-1 || (!strchr(str,'?') && !strchr(str,'*')))
-								return(0); 
+					for(j=0;j<usrdirs[i];j++) {
+						if(msgabort())
+							return 0;
+						if((s=listfileinfo(usrdir[i][j],csi->str,FI_DOWNLOAD))!=0)
+							if(s==-1 || (!strchr(csi->str,'?') && !strchr(csi->str,'*')))
+								return(0);
+					}
 				} 
 			}
 			return(0);
 		case CS_FILE_DOWNLOAD_USER: /* Download from user dir */
 			csi->logic=LOGIC_FALSE;
-			if(cfg.user_dir==INVALID_DIR) {
-				bputs(text[NoUserDir]);
-				return(0); 
-			}
-			if(useron.rest&FLAG('D')) {
-				bputs(text[R_Download]);
-				return(0); 
-			}
-			CRLF;
-			if(!listfileinfo(cfg.user_dir,nulstr,FI_USERXFER))
-				bputs(text[NoFilesForYou]);
-			else
-				csi->logic=LOGIC_TRUE;
+			bputs(text[NoUserDir]);
 			return(0);
 		case CS_FILE_DOWNLOAD_BATCH:
-			if(batdn_total && (text[DownloadBatchQ][0]==0 || yesno(text[DownloadBatchQ]))) {
+			if(batdn_total() > 0
+				&& (text[DownloadBatchQ][0]==0 || yesno(text[DownloadBatchQ]))) {
 				start_batch_download();
 				csi->logic=LOGIC_TRUE; 
 			}
@@ -369,50 +348,36 @@ int sbbs_t::exec_file(csi_t *csi)
 			csi->logic=LOGIC_FALSE;
 			if(!csi->str[0])
 				return(0);
-			padfname(csi->str,f.name);
 			for(x=y=0;x<usrlibs;x++) {
-				for(y=0;y<usrdirs[x];y++)
-					if(findfile(&cfg,usrdir[x][y],f.name))
-						break;
-				if(y<usrdirs[x])
-					break; 
+				for(y=0;y<usrdirs[x];y++) {
+					if(msgabort())
+						return 0;
+					if(loadfile(&cfg, usrdir[x][y], csi->str, &f, file_detail_normal)) {
+						addtobatdl(&f);
+						smb_freefilemem(&f);
+						csi->logic=LOGIC_TRUE;
+						return 0;
+					}
+				}
 			}
-			if(x>=usrlibs)
-				return(0);
-			f.dir=usrdir[x][y];
-			getfileixb(&cfg,&f);
-			f.size=0;
-			getfiledat(&cfg,&f);
-			addtobatdl(&f);
-			csi->logic=LOGIC_TRUE;
 			return(0);
+
 		case CS_FILE_BATCH_CLEAR:
-			if(!batdn_total) {
-				csi->logic=LOGIC_FALSE;
-				return(0); 
-			}
-			csi->logic=LOGIC_TRUE;
-			for(i=0;i<batdn_total;i++) {
-				f.dir=batdn_dir[i];
-				f.datoffset=batdn_offset[i];
-				f.size=batdn_size[i];
-				strcpy(f.name,batdn_name[i]);
-				closefile(&f); 
-			}
-			batdn_total=0;
-			return(0);
+			csi->logic = clearbatdl() ? LOGIC_TRUE : LOGIC_FALSE;
+			return 0; 
 
 		case CS_FILE_VIEW:
 			if(!usrlibs) return(0);
-			padfname(csi->str,str);
-			strupr(str);
 			csi->logic=LOGIC_TRUE;
-			if(listfiles(usrdir[curlib][curdir[curlib]],str,0,FL_VIEW))
+			if(listfiles(usrdir[curlib][curdir[curlib]], csi->str, 0, FL_VIEW)
+				|| strcmp(csi->str, ALLFILES) == 0)
 				return(0);
 			bputs(text[SearchingAllDirs]);
 			for(i=0;i<usrdirs[curlib];i++) {
+				if(msgabort())
+					return 0;
 				if(i==curdir[curlib]) continue;
-				if(listfiles(usrdir[curlib][i],str,0,FL_VIEW))
+				if(listfiles(usrdir[curlib][i],csi->str,0,FL_VIEW))
 					break; 
 			}
 			if(i<usrdirs[curlib])
@@ -420,9 +385,12 @@ int sbbs_t::exec_file(csi_t *csi)
 			bputs(text[SearchingAllLibs]);
 			for(i=0;i<usrlibs;i++) {
 				if(i==curlib) continue;
-				for(j=0;j<usrdirs[i];j++)
-					if(listfiles(usrdir[i][j],str,0,FL_VIEW))
-						return(0); 
+				for(j=0;j<usrdirs[i];j++) {
+					if(msgabort())
+						return 0;
+					if(listfiles(usrdir[i][j],csi->str,0,FL_VIEW))
+						return(0);
+				}
 			}
 			csi->logic=LOGIC_FALSE;
 			bputs(text[FileNotFound]);
@@ -434,9 +402,7 @@ int sbbs_t::exec_file(csi_t *csi)
 				bputs(text[EmptyDir]);
 				return(0); 
 			}
-			padfname(csi->str,str);
-			strupr(str);
-			s=listfiles(usrdir[curlib][curdir[curlib]],str,0,0);
+			s=listfiles(usrdir[curlib][curdir[curlib]], csi->str, 0, 0);
 			if(s>1) {
 				bprintf(text[NFilesListed],s); 
 			}
@@ -444,22 +410,27 @@ int sbbs_t::exec_file(csi_t *csi)
 			return(0);
 		case CS_FILE_LIST_EXTENDED: /* Extended Information on files */
 			if(!usrlibs) return(0);
-			padfname(csi->str,str);
-			strupr(str);
-			if(!listfileinfo(usrdir[curlib][curdir[curlib]],str,FI_INFO)) {
+			if(!listfileinfo(usrdir[curlib][curdir[curlib]], csi->str, FI_INFO)
+				&& strcmp(csi->str, ALLFILES) != 0) {
 				bputs(text[SearchingAllDirs]);
-				for(i=0;i<usrdirs[curlib];i++)
+				for(i=0;i<usrdirs[curlib];i++) {
+					if(msgabort())
+						return 0;
 					if(i!=curdir[curlib] && (s=listfileinfo(usrdir[curlib][i]
-						,str,FI_INFO))!=0)
-						if(s==-1 || (!strchr(str,'?') && !strchr(str,'*')))
+						,csi->str,FI_INFO))!=0)
+						if(s==-1 || (!strchr(csi->str,'?') && !strchr(csi->str,'*')))
 							return(0);
+				}
 				bputs(text[SearchingAllLibs]);
 				for(i=0;i<usrlibs;i++) {
 					if(i==curlib) continue;
-					for(j=0;j<usrdirs[i];j++)
-						if((s=listfileinfo(usrdir[i][j],str,FI_INFO))!=0)
-							if(s==-1 || (!strchr(str,'?') && !strchr(str,'*')))
-								return(0); 
+					for(j=0;j<usrdirs[i];j++) {
+						if(msgabort())
+							return 0;
+						if((s=listfileinfo(usrdir[i][j],csi->str,FI_INFO))!=0)
+							if(s==-1 || (!strchr(csi->str,'?') && !strchr(csi->str,'*')))
+								return(0);
+					}
 				} 
 			}
 			return(0);
@@ -496,27 +467,32 @@ int sbbs_t::exec_file(csi_t *csi)
 				bputs(text[R_RemoveFiles]);
 				return(0); 
 			}
-			padfname(csi->str,str);
-			strupr(str);
-			if(!listfileinfo(usrdir[curlib][curdir[curlib]],str,FI_REMOVE)) {
+			if(!listfileinfo(usrdir[curlib][curdir[curlib]], csi->str, FI_REMOVE)
+				&& strcmp(csi->str, ALLFILES) != 0) {
 				if(cfg.user_dir!=INVALID_DIR
 					&& cfg.user_dir!=usrdir[curlib][curdir[curlib]])
-					if((s=listfileinfo(cfg.user_dir,str,FI_REMOVE))!=0)
-						if(s==-1 || (!strchr(str,'?') && !strchr(str,'*')))
+					if((s=listfileinfo(cfg.user_dir,csi->str,FI_REMOVE))!=0)
+						if(s==-1 || (!strchr(csi->str,'?') && !strchr(csi->str,'*')))
 							return(0);
 				bputs(text[SearchingAllDirs]);
-				for(i=0;i<usrdirs[curlib];i++)
+				for(i=0;i<usrdirs[curlib];i++) {
+					if(msgabort())
+						return 0;
 					if(i!=curdir[curlib] && i!=cfg.user_dir
-						&& (s=listfileinfo(usrdir[curlib][i],str,FI_REMOVE))!=0)
-						if(s==-1 || (!strchr(str,'?') && !strchr(str,'*')))
+						&& (s=listfileinfo(usrdir[curlib][i],csi->str,FI_REMOVE))!=0)
+						if(s==-1 || (!strchr(csi->str,'?') && !strchr(csi->str,'*')))
 							return(0);
+				}
 				bputs(text[SearchingAllLibs]);
 				for(i=0;i<usrlibs;i++) {
 					if(i==curlib || i==cfg.user_dir) continue;
-					for(j=0;j<usrdirs[i]; j++)
-						if((s=listfileinfo(usrdir[i][j],str,FI_REMOVE))!=0)
-							if(s==-1 || (!strchr(str,'?') && !strchr(str,'*')))
-								return(0); 
+					for(j=0;j<usrdirs[i]; j++) {
+						if(msgabort())
+							return 0;
+						if((s=listfileinfo(usrdir[i][j],csi->str,FI_REMOVE))!=0)
+							if(s==-1 || (!strchr(csi->str,'?') && !strchr(csi->str,'*')))
+								return(0);
+					}
 				} 
 			}
 			return(0);
diff --git a/src/sbbs3/execfunc.cpp b/src/sbbs3/execfunc.cpp
index 3eedc2cf2ace3016747dba49986705e6d145f86e..28566807b54ea21c32b5007b6e34797617fbf636 100644
--- a/src/sbbs3/execfunc.cpp
+++ b/src/sbbs3/execfunc.cpp
@@ -25,7 +25,6 @@
 int sbbs_t::exec_function(csi_t *csi)
 {
 	char	str[256];
-	char 	tmp[512];
 	uchar*	p;
 	int		s;
 	uint 	i,j,k;
@@ -302,38 +301,11 @@ int sbbs_t::exec_function(csi_t *csi)
 			}
 			return(0);
 		case CS_FILE_SET_ALT_PATH:
-			altul=atoi(csi->str);
-			if(altul>cfg.altpaths)
-				altul=0;
-			bprintf(text[AltULPathIsNow],altul ? cfg.altpath[altul-1] : text[OFF]);
+			bprintf("Alternate Upload Paths are Unsupported\r\n");
 			return(0);
 		case CS_FILE_RESORT_DIRECTORY:
-			for(i=1;i<=cfg.sys_nodes;i++)
-				if(i!=cfg.node_num) {
-					getnodedat(i,&node,0);
-					if(node.status==NODE_INUSE
-						|| node.status==NODE_QUIET)
-						break; 
-				}
-
-			if(i<=cfg.sys_nodes) {
-				bputs(text[ResortWarning]);
-				return(0); 
-			}
-
-			if(!stricmp(csi->str,"ALL")) {     /* all libraries */
-				for(i=0;i<usrlibs;i++)
-					for(j=0;j<usrdirs[i];j++)
-						resort(usrdir[i][j]);
-				return(0); 
-			}
-			if(!stricmp(csi->str,"LIB")) {     /* current library */
-				for(i=0;i<usrdirs[curlib];i++)
-					resort(usrdir[curlib][i]);
-				return(0); 
-			}
-			resort(usrdir[curlib][curdir[curlib]]);
-			return(0);
+			lprintf(LOG_WARNING, "deprecated function: RESORT_DIRECTORY");
+			return 0;
 
 		case CS_FILE_GET:
 			if(!fexist(csi->str)) {
@@ -387,9 +359,8 @@ int sbbs_t::exec_function(csi_t *csi)
 		case CS_FILE_FIND_OFFLINE:
 		case CS_FILE_FIND_OLD_UPLOADS:
 			if(!usrlibs) return(0);
-			if(!getfilespec(tmp))
+			if(!getfilespec(str))
 				return(0);
-			padfname(tmp,str);
 			k=0;
 			bputs("\r\nSearching ");
 			if(!stricmp(csi->str,"ALL"))
@@ -412,8 +383,8 @@ int sbbs_t::exec_function(csi_t *csi)
 				bputs("not online...\r\n"); 
 			}
 			else {
-				l=FI_CLOSE;
-				bputs("currently open...\r\n"); 
+				// e.g. FI_CLOSE;
+				return 0;
 			}
 			if(!stricmp(csi->str,"ALL")) {
 				for(i=0;i<usrlibs;i++)
diff --git a/src/sbbs3/execmisc.cpp b/src/sbbs3/execmisc.cpp
index 97ce59e1d5a09bc6602c5363a0a410d5ac9c88ec..d19f8a1987b2e10836f0007769000860b6578bb1 100644
--- a/src/sbbs3/execmisc.cpp
+++ b/src/sbbs3/execmisc.cpp
@@ -48,7 +48,7 @@ static char* format_string(sbbs_t* sbbs, csi_t* csi)
 	return xp_asprintf_end(fmt, NULL);
 }
 
-int sbbs_t::exec_misc(csi_t* csi, char *path)
+int sbbs_t::exec_misc(csi_t* csi, const char *path)
 {
 	char	str[512],tmp[512],buf[1025],ch,op,*p,**pp,**pp1,**pp2;
 	ushort	w;
@@ -1576,7 +1576,7 @@ int sbbs_t::exec_misc(csi_t* csi, char *path)
 				free(text[i]);
 			j=strlen(cmdstr((char *)csi->ip,path,csi->str,buf));
 			if(!j)
-				text[i]=nulstr;
+				text[i]=(char*)nulstr;
 			else
 				text[i]=(char *)malloc(j+1);
 			if(!text[i]) {
diff --git a/src/sbbs3/file.cpp b/src/sbbs3/file.cpp
index 6ebc3a64a711d25036e70c2283fb34485d8432a3..725478e6348f59f95f06dc70518d3045d0537c0f 100644
--- a/src/sbbs3/file.cpp
+++ b/src/sbbs3/file.cpp
@@ -1,4 +1,4 @@
-/* Synchronet file transfer-related functions */
+/* Synchronet file transfer-related sbbs_t class methods */
 
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
@@ -20,220 +20,89 @@
  ****************************************************************************/
 
 #include "sbbs.h"
+#include "filedat.h"
 
 /****************************************************************************/
 /* Prints all information of file in file_t structure 'f'					*/
 /****************************************************************************/
-void sbbs_t::fileinfo(file_t* f)
+void sbbs_t::showfileinfo(file_t* f, bool show_extdesc)
 {
-	char	ext[513];
 	char 	tmp[512];
 	char	tmp2[64];
 	char	path[MAX_PATH+1];
-	char	fname[MAX_PATH+1];
-	char*	real_fname;
-	uint	i,j;
 
 	current_file = f;
-	for(i=0;i<usrlibs;i++)
-		if(usrlib[i]==cfg.dir[f->dir]->lib)
-			break;
-	for(j=0;j<usrdirs[i];j++)
-		if(usrdir[i][j]==f->dir)
-			break;
+	getfilepath(&cfg, f, path);
+	bprintf(P_TRUNCATE, text[FiLib], getusrlib(f->dir), cfg.lib[cfg.dir[f->dir]->lib]->lname);
+	bprintf(P_TRUNCATE, text[FiDir], getusrdir(f->dir), cfg.dir[f->dir]->lname);
+	bprintf(P_TRUNCATE, text[FiFilename],f->name);
 
-	getfilepath(&cfg,f,path);
-	real_fname = getfname(path);
-	unpadfname(f->name, fname);
-	bprintf(text[FiLib],i+1,cfg.lib[cfg.dir[f->dir]->lib]->lname);
-	bprintf(text[FiDir],j+1,cfg.dir[f->dir]->lname);
-	bprintf(text[FiFilename],fname);
-	if(strcmp(real_fname, fname) && strcmp(f->desc, real_fname))	/* Different "actual" filename */
-		bprintf(text[FiFilename], real_fname);
-
-	if(f->size!=-1L)
-		bprintf(text[FiFileSize],ultoac(f->size,tmp)
+	if(getfilesize(&cfg, f) >= 0)
+		bprintf(P_TRUNCATE, text[FiFileSize], ultoac((ulong)f->size,tmp)
 			, byte_estimate_to_str(f->size, tmp2, sizeof(tmp2), /* units: */1024, /* precision: */1));
-	bprintf(text[FiCredits]
-		,(cfg.dir[f->dir]->misc&DIR_FREE || !f->cdt) ? "FREE" : ultoac(f->cdt,tmp));
-	bprintf(text[FiDescription],f->desc);
-	bprintf(text[FiUploadedBy],f->misc&FM_ANON ? text[UNKNOWN_USER] : f->uler);
-	if(f->date)
-		bprintf(text[FiFileDate],timestr(f->date));
-	bprintf(text[FiDateUled],timestr(f->dateuled));
-	bprintf(text[FiDateDled],f->datedled ? timestr(f->datedled) : "Never");
-	bprintf(text[FiTimesDled],f->timesdled);
-	if(f->size>0 && f->timetodl>0)
-		bprintf(text[FiTransferTime],sectostr(f->timetodl,tmp));
-	if(f->altpath) {
-		if(f->altpath<=cfg.altpaths) {
-			if(SYSOP)
-				bprintf(text[FiAlternatePath],cfg.altpath[f->altpath-1]); 
+
+	bprintf(P_TRUNCATE, text[FiCredits]
+		,(cfg.dir[f->dir]->misc&DIR_FREE || !f->cost) ? "FREE" : ultoac((ulong)f->cost,tmp));
+	if(getfilesize(&cfg, f) > 0 &&  f->size == f->file_idx.idx.size) {
+#if 0 // I don't think anyone cares about the CRC-16 checksum value of a file
+		if(f->file_idx.hash.flags & SMB_HASH_CRC16) {
+			SAFEPRINTF(tmp, "%04x", f->file_idx.hash.data.crc16);
+			bprintf(P_TRUNCATE, text[FiChecksum], "CRC-16", tmp);
+		}
+#endif
+		if(f->file_idx.hash.flags & SMB_HASH_CRC32) {
+			SAFEPRINTF(tmp, "%08x", f->file_idx.hash.data.crc32);
+			bprintf(P_TRUNCATE, text[FiChecksum], "CRC-32", tmp);
 		}
-		else
-			bprintf(text[InvalidAlternatePathN],f->altpath); 
+		if(f->file_idx.hash.flags & SMB_HASH_MD5)
+			bprintf(P_TRUNCATE, text[FiChecksum], "MD5", MD5_hex(tmp, f->file_idx.hash.data.md5));
+		if(f->file_idx.hash.flags & SMB_HASH_SHA1)
+			bprintf(P_TRUNCATE, text[FiChecksum], "SHA-1", SHA1_hex(tmp, f->file_idx.hash.data.sha1));
 	}
-	bputs(text[FileHdrDescSeparator]);
-	if(f->misc&FM_EXTDESC) {
-		getextdesc(&cfg,f->dir,f->datoffset,ext);
-		putmsg(ext,P_NOATCODES);
-		CRLF; 
+	if(f->desc && f->desc[0])
+		bprintf(P_TRUNCATE, text[FiDescription],f->desc);
+	if(f->tags && f->tags[0])
+		bprintf(P_TRUNCATE, text[FiTags], f->tags);
+	char* p = f->hdr.attr&MSG_ANONYMOUS ? text[UNKNOWN_USER] : f->from;
+	if(p != NULL && *p != '\0')
+		bprintf(P_TRUNCATE, text[FiUploadedBy], p);
+	bprintf(P_TRUNCATE, text[FiDateUled],timestr(f->hdr.when_imported.time));
+	if(getfiletime(&cfg, f) > 0)
+		bprintf(P_TRUNCATE, text[FiFileDate],timestr(f->time));
+	bprintf(P_TRUNCATE, text[FiDateDled],f->hdr.last_downloaded ? timestr(f->hdr.last_downloaded) : "Never");
+	bprintf(P_TRUNCATE, text[FiTimesDled],f->hdr.times_downloaded);
+	ulong timetodl = gettimetodl(&cfg, f, cur_cps);
+	if(timetodl > 0)
+		bprintf(text[FiTransferTime],sectostr(timetodl,tmp));
+	bputs(P_TRUNCATE, text[FileHdrDescSeparator]);
+	if(show_extdesc && f->extdesc != NULL && *f->extdesc) {
+		char* p = f->extdesc;
+		SKIP_CRLF(p);
+		truncsp(p);
+		long p_mode = P_NOATCODES;
+		if(!(console&CON_RAW_IN))
+			p_mode |= P_WORDWRAP;
+		putmsg(p, p_mode);
+		newline();
 	}
-	if(f->size==-1L) {
+	if(f->size == -1) {
 		bprintf(text[FileIsNotOnline],f->name);
 		if(SYSOP)
 			bprintf("%s\r\n",path);
 	}
-	if(f->opencount)
-		bprintf(text[FileIsOpen],f->opencount,f->opencount>1 ? "s" : nulstr);
 	current_file = NULL;
 }
 
-
-/****************************************************************************/
-/* Increments the opencount on the file data 'f' and adds the transaction 	*/
-/* to the backout.dab														*/
-/****************************************************************************/
-void sbbs_t::openfile(file_t* f)
-{
-	char str1[256],str2[4],str3[4],ch;
-	int file;
-
-	/************************************/
-	/* Increment open count in dat file */
-	/************************************/
-	sprintf(str1,"%s%s.dat",cfg.dir[f->dir]->data_dir,cfg.dir[f->dir]->code);
-	if((file=nopen(str1,O_RDWR))==-1) {
-		errormsg(WHERE,ERR_OPEN,str1,O_RDWR);
-		return; 
-	}
-	(void)lseek(file,f->datoffset+F_OPENCOUNT,SEEK_SET);
-	if(read(file,str2,3)!=3) {
-		close(file);
-		errormsg(WHERE,ERR_READ,str1,3);
-		return; 
-	}
-	str2[3]=0;
-	ultoa(atoi(str2)+1,str3,10);
-	putrec(str2,0,3,str3);
-	(void)lseek(file,f->datoffset+F_OPENCOUNT,SEEK_SET);
-	if(write(file,str2,3)!=3) {
-		close(file);
-		errormsg(WHERE,ERR_WRITE,str1,3);
-		return; 
-	}
-	close(file);
-	/**********************************/
-	/* Add transaction to BACKOUT.DAB */
-	/**********************************/
-	sprintf(str1,"%sbackout.dab",cfg.node_dir);
-	if((file=nopen(str1,O_WRONLY|O_APPEND|O_CREAT))==-1) {
-		errormsg(WHERE,ERR_OPEN,str1,O_WRONLY|O_APPEND|O_CREAT);
-		return; 
-	}
-	ch=BO_OPENFILE;
-	write(file,&ch,1);				/* backout type */
-	write(file,cfg.dir[f->dir]->code,8); /* directory code */
-	write(file,&f->datoffset,4);		/* offset into .dat file */
-	write(file,&ch,BO_LEN-(1+8+4)); /* pad it */
-	close(file);
-}
-
-/****************************************************************************/
-/* Decrements the opencount on the file data 'f' and removes the backout  	*/
-/* from the backout.dab														*/
-/****************************************************************************/
-void sbbs_t::closefile(file_t* f)
-{
-	char	str1[256],str2[4],str3[4],ch,*buf;
-	int		file;
-	long	length,l,offset;
-
-	/************************************/
-	/* Decrement open count in dat file */
-	/************************************/
-	sprintf(str1,"%s%s.dat",cfg.dir[f->dir]->data_dir,cfg.dir[f->dir]->code);
-	if((file=nopen(str1,O_RDWR))==-1) {
-		errormsg(WHERE,ERR_OPEN,str1,O_RDWR);
-		return; 
-	}
-	(void)lseek(file,f->datoffset+F_OPENCOUNT,SEEK_SET);
-	if(read(file,str2,3)!=3) {
-		close(file);
-		errormsg(WHERE,ERR_READ,str1,3);
-		return; 
-	}
-	str2[3]=0;
-	ch=atoi(str2);
-	if(ch) ch--;
-	ultoa(ch,str3,10);
-	putrec(str2,0,3,str3);
-	(void)lseek(file,f->datoffset+F_OPENCOUNT,SEEK_SET);
-	if(write(file,str2,3)!=3) {
-		close(file);
-		errormsg(WHERE,ERR_WRITE,str1,3);
-		return; 
-	}
-	close(file);
-	/*****************************************/
-	/* Removing transaction from BACKOUT.DAB */
-	/*****************************************/
-	sprintf(str1,"%sbackout.dab",cfg.node_dir);
-	if(flength(str1)<1L)	/* file is not there or empty */
-		return;
-	if((file=nopen(str1,O_RDONLY))==-1) {
-		errormsg(WHERE,ERR_OPEN,str1,O_RDONLY);
-		return; 
-	}
-	length=(long)filelength(file);
-	if((buf=(char *)malloc(length))==NULL) {
-		close(file);
-		errormsg(WHERE,ERR_ALLOC,str1,length);
-		return; 
-	}
-	if(read(file,buf,length)!=length) {
-		close(file);
-		free(buf);
-		errormsg(WHERE,ERR_READ,str1,length);
-		return; 
-	}
-	close(file);
-	if((file=nopen(str1,O_WRONLY|O_TRUNC))==-1) {
-		free(buf);
-		errormsg(WHERE,ERR_OPEN,str1,O_WRONLY|O_TRUNC);
-		return; 
-	}
-	ch=0;								/* 'ch' is a 'file already removed' flag */
-	for(l=0;l<length;l+=BO_LEN) {       /* in case file is in backout.dab > 1 */
-		if(!ch && buf[l]==BO_OPENFILE) {
-			memcpy(str1,buf+l+1,8);
-			str1[8]=0;
-			memcpy(&offset,buf+l+9,4);
-			if(!stricmp(str1,cfg.dir[f->dir]->code) && offset==f->datoffset) {
-				ch=1;
-				continue; 
-			}
-		}
-		write(file,buf+l,BO_LEN); 
-	}
-	free(buf);
-	close(file);
-}
-
 /****************************************************************************/
-/* Prompts user for file specification. <CR> is *.* and .* is assumed.      */
+/* Prompts user for file specification. <CR> is *							*/
 /* Returns padded file specification.                                       */
 /* Returns NULL if input was aborted.                                       */
 /****************************************************************************/
 char * sbbs_t::getfilespec(char *str)
 {
 	bputs(text[FileSpecStarDotStar]);
-	if(!getstr(str,64,K_NONE))
-		strcpy(str,ALLFILES);
-#if 0
-	else if(!strchr(str,'.') && strlen(str)<=8)
-		strcat(str,".*");
-#endif
+	if(!getstr(str, MAX_FILENAME_LEN, K_NONE))
+		strcpy(str, ALLFILES);
 	if(sys_status&SS_ABORT)
 		return(0);
 	return(str);
@@ -244,19 +113,7 @@ char * sbbs_t::getfilespec(char *str)
 /****************************************************************************/
 extern "C" BOOL filematch(const char *filename, const char *filespec)
 {
-    char c;
-
-	for(c=0;c<8;c++) /* Handle Name */
-		if(filespec[c]=='*') break;
-		else if(filespec[c]=='?') continue;
-		else if(toupper(filename[c])!=toupper(filespec[c])) return(FALSE);
-	if(filespec[8]==' ')	/* no extension specified */
-		return(TRUE);
-	for(c=9;c<12;c++)
-		if(filespec[c]=='*') break;
-		else if(filespec[c]=='?') continue;
-		else if(toupper(filename[c])!=toupper(filespec[c])) return(FALSE);
-	return(TRUE);
+	return wildmatchi(filename, filespec, /* path: */FALSE);
 }
 
 /*****************************************************************************/
@@ -267,17 +124,12 @@ bool sbbs_t::checkfname(char *fname)
     int		c=0,d;
 
 	if(fname[0]=='-'
-		|| strcspn(fname,ILLEGAL_FILENAME_CHARS)!=strlen(fname)) {
+		|| strcspn(fname,ILLEGAL_FILENAME_CHARS)!=strlen(fname)
+		|| strstr(fname, "..") != NULL) {
 		lprintf(LOG_WARNING,"Suspicious filename attempt: '%s'",fname);
 		hacklog((char *)"Filename", fname);
 		return(false); 
 	}
-	if(strstr(fname,".."))
-		return(false);
-#if 0	/* long file name support */
-	if(strcspn(fname,".")>8)
-		return(false);
-#endif
 	d=strlen(fname);
 	while(c<d) {
 		if(fname[c]<=' ' || fname[c]&0x80)
@@ -294,3 +146,112 @@ long sbbs_t::delfiles(const char *inpath, const char *spec, size_t keep)
 		errormsg(WHERE, ERR_REMOVE, inpath, result, spec);
 	return result;
 }
+
+/****************************************************************************/
+/* Remove credits or minutes and adjust statistics of uploader of file 'f'	*/
+/****************************************************************************/
+bool sbbs_t::removefcdt(file_t* f)
+{
+	char	str[128];
+	char 	tmp[512];
+	int		u;
+	long	cdt;
+
+	if((u=matchuser(&cfg,f->from,TRUE /*sysop_alias*/))==0) {
+	   bputs(text[UnknownUser]);
+	   return(false); 
+	}
+	cdt=0L;
+	if(cfg.dir[f->dir]->misc&DIR_CDTMIN && cur_cps) {
+		if(cfg.dir[f->dir]->misc&DIR_CDTUL)
+			cdt=((ulong)(f->cost*(cfg.dir[f->dir]->up_pct/100.0))/cur_cps)/60;
+		if(cfg.dir[f->dir]->misc&DIR_CDTDL
+			&& f->hdr.times_downloaded)  /* all downloads */
+			cdt+=((ulong)((long)f->hdr.times_downloaded
+				*f->cost*(cfg.dir[f->dir]->dn_pct/100.0))/cur_cps)/60;
+		if(cdt) {
+			adjustuserrec(&cfg,u,U_MIN,10,-cdt);
+			sprintf(str,"%lu minute",cdt);
+			sprintf(tmp,text[FileRemovedUserMsg]
+				,f->name,cdt ? str : text[No]);
+			putsmsg(&cfg,u,tmp);
+		}
+	}
+	else {
+		if(cfg.dir[f->dir]->misc&DIR_CDTUL)
+			cdt=(ulong)(f->cost*(cfg.dir[f->dir]->up_pct/100.0));
+		if(cfg.dir[f->dir]->misc&DIR_CDTDL
+			&& f->hdr.times_downloaded)  /* all downloads */
+			cdt+=(ulong)((long)f->hdr.times_downloaded
+				*f->cost*(cfg.dir[f->dir]->dn_pct/100.0));
+		if(dir_op(f->dir)) {
+			ultoa(cdt, str, 10);
+			bputs(text[CreditsToRemove]);
+			getstr(str, 10, K_NUMBER|K_LINE|K_EDIT|K_AUTODEL);
+			if(sys_status&SS_ABORT)
+				return false;
+			cdt = atol(str); 
+		}
+		if(cdt) {
+			adjustuserrec(&cfg,u,U_CDT,10,-cdt);
+			sprintf(tmp,text[FileRemovedUserMsg]
+				,f->name,cdt ? ultoac(cdt,str) : text[No]);
+			putsmsg(&cfg,u,tmp);
+		}
+	}
+
+	adjustuserrec(&cfg,u,U_ULB,10,(long)-f->size);
+	adjustuserrec(&cfg,u,U_ULS,5,-1);
+	return(true);
+}
+
+/****************************************************************************/
+/****************************************************************************/
+bool sbbs_t::removefile(smb_t* smb, file_t* f)
+{
+	char str[256];
+	int result;
+
+	if((result = smb_removefile(smb ,f)) == SMB_SUCCESS) {
+		SAFEPRINTF4(str,"%s removed %s from %s %s"
+			,useron.alias
+			,f->name
+			,cfg.lib[cfg.dir[smb->dirnum]->lib]->sname,cfg.dir[smb->dirnum]->sname);
+		logline("U-",str);
+		return true;
+	}
+	errormsg(WHERE, ERR_REMOVE, f->name, result, smb->last_error);
+	return false;
+}
+
+/****************************************************************************/
+/****************************************************************************/
+bool sbbs_t::movefile(smb_t* smb, file_t* f, int newdir)
+{
+	if(findfile(&cfg, newdir, f->name, NULL)) {
+		bprintf(text[FileAlreadyThere], f->name);
+		return false; 
+	}
+
+	if(!addfile(&cfg, newdir, f, f->extdesc))
+		return false;
+	removefile(smb, f);
+	bprintf(text[MovedFile],f->name
+		,cfg.lib[cfg.dir[newdir]->lib]->sname,cfg.dir[newdir]->sname);
+	char str[MAX_PATH+1];
+	SAFEPRINTF4(str, "%s moved %s to %s %s",f->name
+		,useron.alias
+		,cfg.lib[cfg.dir[newdir]->lib]->sname
+		,cfg.dir[newdir]->sname);
+	logline(nulstr,str);
+
+	/* move actual file */
+	char oldpath[MAX_PATH + 1];
+	getfilepath(&cfg, f, oldpath);
+	f->dir = newdir;
+	char newpath[MAX_PATH + 1];
+	getfilepath(&cfg, f, newpath);
+	mv(oldpath, newpath, /* copy */false); 
+	
+	return true;
+}
diff --git a/src/sbbs3/filedat.c b/src/sbbs3/filedat.c
index 11f8e7625e84604e856ec43d6c15ad179b965e65..1007b5b2e0694f5b90d1d4362b358b704f91eea6 100644
--- a/src/sbbs3/filedat.c
+++ b/src/sbbs3/filedat.c
@@ -20,711 +20,1095 @@
  ****************************************************************************/
 
 #include "filedat.h"
+#include "userdat.h"
 #include "dat_rec.h"
 #include "datewrap.h"	// time32()
 #include "str_util.h"
 #include "nopen.h"
+#include "smblib.h"
+#include "load_cfg.h"	// smb_open_dir()
+#include "scfglib.h"
 
-static char* crlf = "\r\n";
+/* libarchive: */
+#include <archive.h>
+#include <archive_entry.h>
 
+ /****************************************************************************/
 /****************************************************************************/
-/* Gets filedata from dircode.DAT file										*/
-/* Need fields .name ,.dir and .offset to get other info    				*/
-/* Does not fill .dateuled or .datedled fields.                             */
-/****************************************************************************/
-BOOL DLLCALL getfiledat(scfg_t* cfg, file_t* f)
+bool findfile(scfg_t* cfg, uint dirnum, const char *filename, file_t* file)
 {
-	char buf[F_LEN+1],str[MAX_PATH+1];
-	int file;
-	long length;
+	smb_t smb;
 
-	SAFEPRINTF2(str,"%s%s.dat",cfg->dir[f->dir]->data_dir,cfg->dir[f->dir]->code);
-	if((file=sopen(str,O_RDONLY|O_BINARY,SH_DENYWR))==-1) {
-		return(FALSE); 
-	}
-	length=(long)filelength(file);
-	if(f->datoffset>length) {
-		close(file);
-		return(FALSE); 
-	}
-	if(length%F_LEN) {
-		close(file);
-		return(FALSE); 
-	}
-	lseek(file,f->datoffset,SEEK_SET);
-	if(read(file,buf,F_LEN)!=F_LEN) {
-		close(file);
-		return(FALSE); 
-	}
-	close(file);
-	getrec(buf,F_ALTPATH,2,str);
-	f->altpath=hptoi(str);
-	getrec(buf,F_CDT,LEN_FCDT,str);
-	f->cdt=atol(str);
+	if(cfg == NULL || filename == NULL)
+		return false;
 
-	if(f->size == 0) {					// only read disk if f->size == 0
-		struct stat st;
-		getfilepath(cfg,f,str);
-		if(stat(str, &st) == 0) {
-			f->size = st.st_size;
-			f->date = (time32_t)st.st_mtime;
-		} else
-			f->size = -1;	// indicates file does not exist
-	}
-#if 0
-	if((f->size>0L) && cur_cps)
-		f->timetodl=(ushort)(f->size/(ulong)cur_cps);
-	else
-#endif
-		f->timetodl=0;
+	if(!smb_init_dir(cfg, &smb, dirnum))
+		return false;
+	if(smb_open_index(&smb) != SMB_SUCCESS)
+		return false;
+	int result = smb_findfile(&smb, filename, file);
+	smb_close(&smb);
+	return result == SMB_SUCCESS;
+}
 
-	getrec(buf,F_DESC,LEN_FDESC,f->desc);
-	getrec(buf,F_ULER,LEN_ALIAS,f->uler);
-	getrec(buf,F_TIMESDLED,5,str);
-	f->timesdled=atoi(str);
-	getrec(buf,F_OPENCOUNT,3,str);
-	f->opencount=atoi(str);
-	if(buf[F_MISC]!=ETX)
-		f->misc=buf[F_MISC]-' ';
-	else
-		f->misc=0;
-	return(TRUE);
+// This function may be called without opening the file base (for fast new-scans)
+time_t newfiletime(smb_t* smb)
+{
+	char str[MAX_PATH + 1];
+	SAFEPRINTF(str, "%s.sid", smb->file);
+	return fdate(str);
 }
 
-/****************************************************************************/
-/* Puts filedata into DIR_code.DAT file                                     */
-/* Called from removefiles                                                  */
-/****************************************************************************/
-BOOL DLLCALL putfiledat(scfg_t* cfg, file_t* f)
-{
-    char buf[F_LEN+1],str[MAX_PATH+1],tmp[128];
-    int file;
-    long length;
-
-	putrec(buf,F_CDT,LEN_FCDT,ultoa(f->cdt,tmp,10));
-	putrec(buf,F_DESC,LEN_FDESC,f->desc);
-	putrec(buf,F_DESC+LEN_FDESC,2,crlf);
-	putrec(buf,F_ULER,LEN_ALIAS+5,f->uler);
-	putrec(buf,F_ULER+LEN_ALIAS+5,2,crlf);
-	putrec(buf,F_TIMESDLED,5,ultoa(f->timesdled,tmp,10));
-	putrec(buf,F_TIMESDLED+5,2,crlf);
-	putrec(buf,F_OPENCOUNT,3,ultoa(f->opencount,tmp,10));
-	putrec(buf,F_OPENCOUNT+3,2,crlf);
-	buf[F_MISC]=(char)f->misc+' ';
-	putrec(buf,F_ALTPATH,2,hexplus(f->altpath,tmp));
-	putrec(buf,F_ALTPATH+2,2,crlf);
-	SAFEPRINTF2(str,"%s%s.dat",cfg->dir[f->dir]->data_dir,cfg->dir[f->dir]->code);
-	if((file=sopen(str,O_WRONLY|O_BINARY,SH_DENYRW))==-1) {
-		return(FALSE); 
-	}
-	length=(long)filelength(file);
-	if(length%F_LEN) {
-		close(file);
-		return(FALSE); 
-	}
-	if(f->datoffset>length) {
-		close(file);
-		return(FALSE); 
-	}
-	lseek(file,f->datoffset,SEEK_SET);
-	if(write(file,buf,F_LEN)!=F_LEN) {
-		close(file);
-		return(FALSE); 
-	}
-	length=(long)filelength(file);
-	close(file);
-	if(length%F_LEN) {
-		return(FALSE);
+time_t dir_newfiletime(scfg_t* cfg, uint dirnum)
+{
+	smb_t smb;
+
+	if(!smb_init_dir(cfg, &smb, dirnum))
+		return -1;
+	return newfiletime(&smb);
+}
+
+// This function may be called without opening the file base (for fast new-scans)
+bool newfiles(smb_t* smb, time_t t)
+{
+	return newfiletime(smb) > t;
+}
+
+// This function *must* be called with file base closed
+bool update_newfiletime(smb_t* smb, time_t t)
+{
+	char path[MAX_PATH + 1];
+	SAFEPRINTF(path, "%s.sid", smb->file);
+	return setfdate(path, t) == 0;
+}
+
+time_t lastfiletime(smb_t* smb)
+{
+	idxrec_t idx;
+	if(smb_getlastidx(smb, &idx) != SMB_SUCCESS)
+		return (time_t)-1;
+	return idx.time;
+}
+
+static int filename_compare_a(const void *arg1, const void *arg2)
+{
+   return stricmp(*(char**) arg1, *(char**) arg2);
+}
+
+static int filename_compare_d(const void *arg1, const void *arg2)
+{
+   return stricmp(*(char**) arg2, *(char**) arg1);
+}
+
+static int filename_compare_ac(const void *arg1, const void *arg2)
+{
+   return strcmp(*(char**) arg1, *(char**) arg2);
+}
+
+static int filename_compare_dc(const void *arg1, const void *arg2)
+{
+   return strcmp(*(char**) arg2, *(char**) arg1);
+}
+
+// Note: does not support sorting by date
+void sortfilenames(str_list_t filelist, size_t count, enum file_sort order)
+{
+	switch(order) {
+		case FILE_SORT_NAME_A:
+			qsort(filelist, count, sizeof(*filelist), filename_compare_a);
+			break;
+		case FILE_SORT_NAME_D:
+			qsort(filelist, count, sizeof(*filelist), filename_compare_d);
+			break;
+		case FILE_SORT_NAME_AC:
+			qsort(filelist, count, sizeof(*filelist), filename_compare_ac);
+			break;
+		case FILE_SORT_NAME_DC:
+			qsort(filelist, count, sizeof(*filelist), filename_compare_dc);
+			break;
+		default:
+			break;
 	}
-	return(TRUE);
 }
 
-/****************************************************************************/
-/* Adds the data for struct filedat to the directory's data base.           */
-/* changes the .datoffset field only                                        */
-/* returns 1 if added successfully, 0 if not.								*/
-/****************************************************************************/
-BOOL DLLCALL addfiledat(scfg_t* cfg, file_t* f)
-{
-	char	str[MAX_PATH+1],fname[13],c,fdat[F_LEN+1];
-	char	tmp[128];
-	uchar	*ixbbuf,idx[3];
-    int		i,file;
-	long	l,length;
-	time_t	uldate;
-
-	/************************/
-	/* Add data to DAT File */
-	/************************/
-	SAFEPRINTF2(str,"%s%s.dat",cfg->dir[f->dir]->data_dir,cfg->dir[f->dir]->code);
-	if((file=sopen(str,O_RDWR|O_BINARY|O_CREAT,SH_DENYRW,DEFFILEMODE))==-1) {
-		return(FALSE); 
+// Return an optionally-sorted dynamically-allocated string list of filenames added since date/time (t)
+// Note: does not support sorting by date (exception: natural sort order of date-ascending)
+str_list_t loadfilenames(smb_t* smb, const char* filespec, time_t t, enum file_sort order, size_t* count)
+{
+	size_t count_;
+
+	if(count == NULL)
+		count = &count_;
+
+	*count = 0;
+
+	long start = 0;	
+	if(t > 0) {
+		idxrec_t idx;
+		start = smb_getmsgidx_by_time(smb, &idx, t);
+		if(start < 0)
+			return NULL;
 	}
-	length=(long)filelength(file);
-	if(length==0L)
-		l=0L;
-	else {
-		if(length%F_LEN) {
-			close(file);
-			return(FALSE); 
-		}
-		for(l=0;l<length;l+=F_LEN) {    /* Find empty slot */
-			lseek(file,l,SEEK_SET);
-			read(file,&c,1);
-			if(c==ETX) break; 
+
+	char** file_list = calloc(smb->status.total_files + 1, sizeof(char*));
+	if(file_list == NULL)
+		return NULL;
+
+	fseek(smb->sid_fp, start * sizeof(fileidxrec_t), SEEK_SET);
+	while(!feof(smb->sid_fp)) {
+		fileidxrec_t fidx;
+
+		if(smb_fread(smb, &fidx, sizeof(fidx), smb->sid_fp) != sizeof(fidx))
+			break;
+
+		if(fidx.idx.number == 0)	/* invalid message number, ignore */
+			continue;
+
+		TERMINATE(fidx.name);
+
+		if(filespec != NULL && *filespec != '\0') {
+			if(!wildmatch(fidx.name, filespec, /* path: */false, /* case-sensitive: */false))
+				continue;
 		}
-		if(l/F_LEN>=MAX_FILES) {
-			close(file);
-			return(FALSE); 
-		} 
-	}
-	putrec(fdat,F_CDT,LEN_FCDT,ultoa(f->cdt,tmp,10));
-	putrec(fdat,F_DESC,LEN_FDESC,f->desc);
-	putrec(fdat,F_DESC+LEN_FDESC,2,crlf);
-	putrec(fdat,F_ULER,LEN_ALIAS+5,f->uler);
-	putrec(fdat,F_ULER+LEN_ALIAS+5,2,crlf);
-	putrec(fdat,F_TIMESDLED,5,ultoa(f->timesdled,tmp,10));
-	putrec(fdat,F_TIMESDLED+5,2,crlf);
-	putrec(fdat,F_OPENCOUNT,3,ultoa(f->opencount,tmp,10));
-	putrec(fdat,F_OPENCOUNT+3,2,crlf);
-	fdat[F_MISC]=(char)f->misc+' ';
-	putrec(fdat,F_ALTPATH,2,hexplus(f->altpath,tmp));
-	putrec(fdat,F_ALTPATH+2,2,crlf);
-	f->datoffset=l;
-	idx[0]=(uchar)(l&0xff);          /* Get offset within DAT file for IXB file */
-	idx[1]=(uchar)((l>>8)&0xff);
-	idx[2]=(uchar)((l>>16)&0xff);
-	lseek(file,l,SEEK_SET);
-	if(write(file,fdat,F_LEN)!=F_LEN) {
-		close(file);
-		return(FALSE); 
-	}
-	length=(long)filelength(file);
-	close(file);
-	if(length%F_LEN) {
-		return(FALSE);
+		file_list[*count] = strdup(fidx.name);
+		(*count)++;
 	}
+	if(order != FILE_SORT_NATURAL)
+		sortfilenames(file_list, *count, order);
 
-	/*******************************************/
-	/* Update last upload date/time stamp file */
-	/*******************************************/
-	SAFEPRINTF2(str,"%s%s.dab",cfg->dir[f->dir]->data_dir,cfg->dir[f->dir]->code);
-	if((file=sopen(str,O_WRONLY|O_CREAT|O_BINARY,SH_DENYRW,DEFFILEMODE))!=-1) {
-		time32_t now=time32(NULL);
-		/* TODO: LE required */
-		write(file,&now,sizeof(time32_t));
-		close(file); 
-	}
+	return file_list;
+}
 
-	/************************/
-	/* Add data to IXB File */
-	/************************/
-	SAFECOPY(fname,f->name);
-	for(i=8;i<12;i++)   /* Turn FILENAME.EXT into FILENAMEEXT */
-		fname[i]=fname[i+1];
-	SAFEPRINTF2(str,"%s%s.ixb",cfg->dir[f->dir]->data_dir,cfg->dir[f->dir]->code);
-	if((file=sopen(str,O_RDWR|O_CREAT|O_BINARY,SH_DENYRW,DEFFILEMODE))==-1) {
-		return(FALSE); 
-	}
-	length=(long)filelength(file);
-	if(length) {    /* IXB file isn't empty */
-		if(length%F_IXBSIZE) {
-			close(file);
-			return(FALSE); 
-		}
-		if((ixbbuf=(uchar *)malloc(length))==NULL) {
-			close(file);
-			return(FALSE); 
-		}
-		if(read(file,ixbbuf,length)!=length) {
-			close(file);
-			free(ixbbuf);
-			return(FALSE); 
-		}
-	/************************************************/
-	/* Sort by Name or Date, Assending or Decending */
-	/************************************************/
-		if(cfg->dir[f->dir]->sort==SORT_NAME_A || cfg->dir[f->dir]->sort==SORT_NAME_D) {
-			for(l=0;l<length;l+=F_IXBSIZE) {
-				for(i=0;i<12 && toupper(fname[i])==toupper(ixbbuf[l+i]);i++);
-				if(i==12) {     /* file already in directory index */
-					close(file);
-					free(ixbbuf);
-					return(FALSE); 
-				}
-				if(cfg->dir[f->dir]->sort==SORT_NAME_A 
-					&& toupper(fname[i])<toupper(ixbbuf[l+i]))
-					break;
-				if(cfg->dir[f->dir]->sort==SORT_NAME_D 
-					&& toupper(fname[i])>toupper(ixbbuf[l+i]))
-					break; 
-			} 
-		}
-		else {  /* sort by date */
-			for(l=0;l<length;l+=F_IXBSIZE) {
-				uldate=(ixbbuf[l+14]|((long)ixbbuf[l+15]<<8)
-					|((long)ixbbuf[l+16]<<16)|((long)ixbbuf[l+17]<<24));
-				if(cfg->dir[f->dir]->sort==SORT_DATE_A && f->dateuled<uldate)
-					break;
-				if(cfg->dir[f->dir]->sort==SORT_DATE_D && f->dateuled>uldate)
-					break; 
-			} 
-		}
-		lseek(file,l,SEEK_SET);
-		if(write(file,fname,11)!=11) {  /* Write filename to IXB file */
-			close(file);
-			free(ixbbuf);
-			return(FALSE); 
-		}
-		if(write(file,idx,3)!=3) {  /* Write DAT offset into IXB file */
-			close(file);
-			free(ixbbuf);
-			return(FALSE); 
-		}
-		write(file,&f->dateuled,4);
-		write(file,&f->datedled,4);              /* Write 0 for datedled */
-		if(write(file,&ixbbuf[l],length-l)!=length-l) { /* Write rest of IXB */
-			close(file);
-			free(ixbbuf);
-			return(FALSE); 
-		}
-		free(ixbbuf); 
+// Load and optionally-sort files from an open filebase into a dynamically-allocated list of "objects"
+file_t* loadfiles(smb_t* smb, const char* filespec, time_t t, enum file_detail detail, enum file_sort order, size_t* count)
+{
+	*count = 0;
+
+	long start = 0;	
+	if(t) {
+		idxrec_t idx;
+		start = smb_getmsgidx_by_time(smb, &idx, t);
+		if(start < 0)
+			return NULL;
 	}
-	else {              /* IXB file is empty... No files */
-		if(write(file,fname,11)!=11) {  /* Write filename it IXB file */
-			close(file);
-			return(FALSE); 
-		}
-		if(write(file,idx,3)!=3) {  /* Write DAT offset into IXB file */
-			close(file);
-			return(FALSE); 
+
+	file_t* file_list = calloc(smb->status.total_files, sizeof(file_t));
+	if(file_list == NULL)
+		return NULL;
+
+	fseek(smb->sid_fp, start * sizeof(fileidxrec_t), SEEK_SET);
+	long offset = start;
+	while(!feof(smb->sid_fp)) {
+		file_t* f = &file_list[*count];
+
+		if(smb_fread(smb, &f->file_idx, sizeof(f->file_idx), smb->sid_fp) != sizeof(f->file_idx))
+			break;
+
+		f->idx_offset = offset++;
+
+		if(f->idx.number == 0)	/* invalid message number, ignore */
+			continue;
+
+		TERMINATE(f->file_idx.name);
+
+		if(filespec != NULL && *filespec != '\0') {
+			if(!wildmatch(f->file_idx.name, filespec, /* path: */false, /* case-sensitive: */false))
+				continue;
 		}
-		write(file,&f->dateuled,4);
-		write(file,&f->datedled,4); 
+		int result = smb_getfile(smb, f, detail);
+		if(result != SMB_SUCCESS)
+			break;
+		(*count)++;
 	}
-	length=(long)filelength(file);
-	close(file);
-	return(TRUE);
+	if(order != FILE_SORT_NATURAL)
+		sortfiles(file_list, *count, order);
+
+	return file_list;
 }
 
-/****************************************************************************/
-/* Gets file data from dircode.ixb file										*/
-/* Need fields .name and .dir filled.                                       */
-/* only fills .offset, .dateuled, and .datedled                             */
-/****************************************************************************/
-BOOL DLLCALL getfileixb(scfg_t* cfg, file_t* f)
+static int file_compare_name_a(const void* v1, const void* v2)
 {
-	char			str[MAX_PATH+1],fname[13];
-	uchar *	ixbbuf;
-	int				file;
-	long			l,length;
+	file_t* f1 = (file_t*)v1;
+	file_t* f2 = (file_t*)v2;
 
-	SAFEPRINTF2(str,"%s%s.ixb",cfg->dir[f->dir]->data_dir,cfg->dir[f->dir]->code);
-	if((file=sopen(str,O_RDONLY|O_BINARY,SH_DENYWR))==-1) {
-		return(FALSE); 
-	}
-	length=(long)filelength(file);
-	if(length%F_IXBSIZE) {
-		close(file);
-		return(FALSE); 
-	}
-	if((ixbbuf=(uchar *)malloc(length))==NULL) {
-		close(file);
-		return(FALSE); 
-	}
-	if(read(file,ixbbuf,length)!=length) {
-		close(file);
-		free(ixbbuf);
-		return(FALSE); 
-	}
-	close(file);
-	SAFECOPY(fname,f->name);
-	for(l=8;l<12;l++)	/* Turn FILENAME.EXT into FILENAMEEXT */
-		fname[l]=fname[l+1];
-	for(l=0;l<length;l+=F_IXBSIZE) {
-		SAFEPRINTF(str,"%11.11s",ixbbuf+l);
-		if(!stricmp(str,fname))
-			break; 
-	}
-	if(l>=length) {
-		free(ixbbuf);
-		return(FALSE); 
-	}
-	l+=11;
-	f->datoffset=ixbbuf[l]|((long)ixbbuf[l+1]<<8)|((long)ixbbuf[l+2]<<16);
-	f->dateuled=ixbbuf[l+3]|((long)ixbbuf[l+4]<<8)
-		|((long)ixbbuf[l+5]<<16)|((long)ixbbuf[l+6]<<24);
-	f->datedled=ixbbuf[l+7]|((long)ixbbuf[l+8]<<8)
-		|((long)ixbbuf[l+9]<<16)|((long)ixbbuf[l+10]<<24);
-	free(ixbbuf);
-	return(TRUE);
+	return stricmp(f1->name, f2->name);
 }
 
-/****************************************************************************/
-/* Updates the datedled and dateuled index record fields for a file			*/
-/****************************************************************************/
-BOOL DLLCALL putfileixb(scfg_t* cfg, file_t* f)
+static int file_compare_name_ac(const void* v1, const void* v2)
 {
-	char	str[MAX_PATH+1],fname[13];
-	uchar*	ixbbuf;
-	int		file;
-	long	l,length;
+	file_t* f1 = (file_t*)v1;
+	file_t* f2 = (file_t*)v2;
 
-	SAFEPRINTF2(str,"%s%s.ixb",cfg->dir[f->dir]->data_dir,cfg->dir[f->dir]->code);
-	if((file=sopen(str,O_RDWR|O_BINARY,SH_DENYRW))==-1) {
-		return(FALSE); 
-	}
-	length=(long)filelength(file);
-	if(length%F_IXBSIZE) {
-		close(file);
-		return(FALSE); 
-	}
-	if((ixbbuf=(uchar *)malloc(length))==NULL) {
-		close(file);
-		return(FALSE); 
-	}
-	if(read(file,ixbbuf,length)!=length) {
-		close(file);
-		free(ixbbuf);
-		return(FALSE); 
-	}
-	SAFECOPY(fname,f->name);
-	for(l=8;l<12;l++)	/* Turn FILENAME.EXT into FILENAMEEXT */
-		fname[l]=fname[l+1];
-	for(l=0;l<length;l+=F_IXBSIZE) {
-		SAFEPRINTF(str,"%11.11s",ixbbuf+l);
-		if(!stricmp(str,fname))
+	return strcmp(f1->name, f2->name);
+}
+
+static int file_compare_name_d(const void* v1, const void* v2)
+{
+	file_t* f1 = (file_t*)v1;
+	file_t* f2 = (file_t*)v2;
+
+	return stricmp(f2->name, f1->name);
+}
+
+static int file_compare_name_dc(const void* v1, const void* v2)
+{
+	file_t* f1 = (file_t*)v1;
+	file_t* f2 = (file_t*)v2;
+
+	return strcmp(f2->name, f1->name);
+}
+
+static int file_compare_date_a(const void* v1, const void* v2)
+{
+	file_t* f1 = (file_t*)v1;
+	file_t* f2 = (file_t*)v2;
+
+	return f1->hdr.when_imported.time - f2->hdr.when_imported.time;
+}
+
+static int file_compare_date_d(const void* v1, const void* v2)
+{
+	file_t* f1 = (file_t*)v1;
+	file_t* f2 = (file_t*)v2;
+
+	return f2->hdr.when_imported.time - f1->hdr.when_imported.time;
+}
+
+void sortfiles(file_t* filelist, size_t count, enum file_sort order)
+{
+	switch(order) {
+		case FILE_SORT_NAME_A:
+			qsort(filelist, count, sizeof(*filelist), file_compare_name_a);
+			break;
+		case FILE_SORT_NAME_D:
+			qsort(filelist, count, sizeof(*filelist), file_compare_name_d);
+			break;
+		case FILE_SORT_NAME_AC:
+			qsort(filelist, count, sizeof(*filelist), file_compare_name_ac);
+			break;
+		case FILE_SORT_NAME_DC:
+			qsort(filelist, count, sizeof(*filelist), file_compare_name_dc);
+			break;
+		case FILE_SORT_DATE_A:
+			qsort(filelist, count, sizeof(*filelist), file_compare_date_a);
+			break;
+		case FILE_SORT_DATE_D:
+			qsort(filelist, count, sizeof(*filelist), file_compare_date_d);
 			break; 
 	}
-	free(ixbbuf);
+}
 
-	if(l>=length) {
-		close(file);
-		return(FALSE); 
-	}
-	
-	lseek(file,l+11+3,SEEK_SET);
+void freefiles(file_t* filelist, size_t count)
+{
+	for(size_t i = 0; i < count; i++)
+		smb_freefilemem(&filelist[i]);
+	free(filelist);
+}
 
-	write(file,&f->dateuled,4);
-	write(file,&f->datedled,4);
+bool loadfile(scfg_t* cfg, uint dirnum, const char* filename, file_t* file, enum file_detail detail)
+{
+	smb_t smb;
 
-	close(file);
+	if(smb_open_dir(cfg, &smb, dirnum) != SMB_SUCCESS)
+		return false;
 
-	return(TRUE);
+	int result = smb_loadfile(&smb, filename, file, detail);
+	smb_close(&smb);
+	if(cfg->dir[dirnum]->misc & DIR_FREE)
+		file->cost = 0;
+	return result == SMB_SUCCESS;
 }
 
+char* batch_list_name(scfg_t* cfg, uint usernumber, enum XFER_TYPE type, char* fname, size_t size)
+{
+	safe_snprintf(fname, size, "%suser/%04u.%sload", cfg->data_dir, usernumber
+		,(type == XFER_UPLOAD || type == XFER_BATCH_UPLOAD) ? "up" : "dn");
+	return fname;
+}
 
-/****************************************************************************/
-/* Removes DAT and IXB entries for the file in the struct 'f'               */
-/****************************************************************************/
-BOOL DLLCALL removefiledat(scfg_t* cfg, file_t* f)
-{
-	char	c,str[MAX_PATH+1],ixbname[12],*ixbbuf,fname[13];
-    int		i,file;
-	long	l,length;
-
-	SAFECOPY(fname,f->name);
-	for(i=8;i<12;i++)   /* Turn FILENAME.EXT into FILENAMEEXT */
-		fname[i]=fname[i+1];
-	SAFEPRINTF2(str,"%s%s.ixb",cfg->dir[f->dir]->data_dir,cfg->dir[f->dir]->code);
-	if((file=sopen(str,O_RDONLY|O_BINARY,SH_DENYWR))==-1) {
-		return(FALSE); 
-	}
-	length=(long)filelength(file);
-	if(!length) {
-		close(file);
-		return(FALSE); 
-	}
-	if((ixbbuf=(char *)malloc(length))==0) {
-		close(file);
-		return(FALSE); 
-	}
-	if(read(file,ixbbuf,length)!=length) {
-		close(file);
-		free(ixbbuf);
-		return(FALSE); 
-	}
-	close(file);
-	if((file=sopen(str,O_WRONLY|O_TRUNC|O_BINARY,SH_DENYRW))==-1) {
-		free(ixbbuf);
-		return(FALSE); 
-	}
-	for(l=0;l<length;l+=F_IXBSIZE) {
-		for(i=0;i<11;i++)
-			ixbname[i]=ixbbuf[l+i];
-		ixbname[i]=0;
-		if(stricmp(ixbname,fname))
-			if(write(file,&ixbbuf[l],F_IXBSIZE)!=F_IXBSIZE) {
-				close(file);
-				free(ixbbuf);
-				return(FALSE); 
-		} 
-	}
-	free(ixbbuf);
-	close(file);
-	SAFEPRINTF2(str,"%s%s.dat",cfg->dir[f->dir]->data_dir,cfg->dir[f->dir]->code);
-	if((file=sopen(str,O_WRONLY|O_BINARY,SH_DENYRW))==-1) {
-		return(FALSE); 
-	}
-	lseek(file,f->datoffset,SEEK_SET);
-	c=ETX;          /* If first char of record is ETX, record is unused */
-	if(write(file,&c,1)!=1) { /* So write a D_T on the first byte of the record */
-		close(file);
-		return(FALSE); 
+FILE* batch_list_open(scfg_t* cfg, uint usernumber, enum XFER_TYPE type, bool create)
+{
+	char path[MAX_PATH + 1];
+	return iniOpenFile(batch_list_name(cfg, usernumber, type, path, sizeof(path)), create);
+}
+
+str_list_t batch_list_read(scfg_t* cfg, uint usernumber, enum XFER_TYPE type)
+{
+	FILE* fp = batch_list_open(cfg, usernumber, type, /* create: */false);
+	if(fp == NULL)
+		return NULL;
+	str_list_t ini = iniReadFile(fp);
+	iniCloseFile(fp);
+	return ini;
+}
+
+bool batch_list_write(scfg_t* cfg, uint usernumber, enum XFER_TYPE type, str_list_t list)
+{
+	FILE* fp = batch_list_open(cfg, usernumber, type, /* create: */true);
+	if(fp == NULL)
+		return false;
+	bool result = iniWriteFile(fp, list);
+	iniCloseFile(fp);
+	return result;
+}
+
+bool batch_list_clear(scfg_t* cfg, uint usernumber, enum XFER_TYPE type)
+{
+	char path[MAX_PATH + 1];
+	return remove(batch_list_name(cfg, usernumber, type, path, sizeof(path))) == 0;
+}
+
+size_t batch_file_count(scfg_t* cfg, uint usernumber, enum XFER_TYPE type)
+{
+	FILE* fp = batch_list_open(cfg, usernumber, type, /* create: */false);
+	if(fp == NULL)
+		return 0;
+	size_t result = iniReadSectionCount(fp, /* prefix: */NULL);
+	iniCloseFile(fp);
+	return result;
+}
+
+bool batch_file_remove(scfg_t* cfg, uint usernumber, enum XFER_TYPE type, const char* filename)
+{
+	FILE* fp = batch_list_open(cfg, usernumber, type, /* create: */false);
+	if(fp == NULL)
+		return false;
+	str_list_t ini = iniReadFile(fp);
+	bool result = iniRemoveSection(&ini, filename);
+	iniWriteFile(fp, ini);
+	iniCloseFile(fp);
+	iniFreeStringList(ini);
+	return result;
+}
+
+bool batch_file_exists(scfg_t* cfg, uint usernumber, enum XFER_TYPE type, const char* filename)
+{
+	FILE* fp = batch_list_open(cfg, usernumber, type, /* create: */false);
+	if(fp == NULL)
+		return false;
+	str_list_t ini = iniReadFile(fp);
+	bool result = iniSectionExists(ini, filename);
+	iniCloseFile(fp);
+	iniFreeStringList(ini);
+	return result;
+}
+
+bool batch_file_add(scfg_t* cfg, uint usernumber, enum XFER_TYPE type, file_t* f)
+{
+	FILE* fp = batch_list_open(cfg, usernumber, type, /* create: */true);
+	if(fp == NULL)
+		return false;
+	fseek(fp, 0, SEEK_END);
+	fprintf(fp, "\n[%s]\n", f->name);
+	if(f->dir >= 0 && f->dir < cfg->total_dirs)
+		fprintf(fp, "dir=%s\n", cfg->dir[f->dir]->code);
+	if(f->desc != NULL)
+		fprintf(fp, "desc=%s\n", f->desc);
+	if(f->tags != NULL)
+		fprintf(fp, "tags=%s\n", f->tags);
+	fclose(fp);
+	return true;
+}
+
+bool batch_file_get(scfg_t* cfg, str_list_t ini, const char* filename, file_t* f)
+{
+	char* p;
+	char value[INI_MAX_VALUE_LEN + 1];
+
+	if(!iniSectionExists(ini, filename))
+		return false;
+	f->dir = batch_file_dir(cfg, ini, filename);
+	if(f->dir < 0 || f->dir >= cfg->total_dirs)
+		return false;
+	smb_hfield_str(f, SMB_FILENAME, filename);
+	if((p = iniGetString(ini, filename, "desc", NULL, value)) != NULL)
+		smb_hfield_str(f, SMB_FILEDESC, p);
+	if((p = iniGetString(ini, filename, "tags", NULL, value)) != NULL)
+		smb_hfield_str(f, SMB_TAGS, p);
+	return true;
+}
+
+int batch_file_dir(scfg_t* cfg, str_list_t ini, const char* filename)
+{
+	char value[INI_MAX_VALUE_LEN + 1];
+	return getdirnum(cfg, iniGetString(ini, filename, "dir", NULL, value));
+}
+
+bool batch_file_load(scfg_t* cfg, str_list_t ini, const char* filename, file_t* f)
+{
+	if(!iniSectionExists(ini, filename))
+		return false;
+	f->dir = batch_file_dir(cfg, ini, filename);
+	if(f->dir < 0)
+		return false;
+	return loadfile(cfg, f->dir, filename, f, file_detail_normal);
+}
+
+bool updatefile(scfg_t* cfg, file_t* file)
+{
+	smb_t smb;
+
+	if(smb_open_dir(cfg, &smb, file->dir) != SMB_SUCCESS)
+		return false;
+
+	int result = smb_updatemsg(&smb, file) == SMB_SUCCESS;
+	smb_close(&smb);
+	return result == SMB_SUCCESS;
+}
+
+bool removefile(scfg_t* cfg, uint dirnum, const char* filename)
+{
+	smb_t smb;
+
+	if(smb_open_dir(cfg, &smb, dirnum) != SMB_SUCCESS)
+		return false;
+
+	int result;
+	file_t file;
+	if((result = smb_loadfile(&smb, filename, &file, file_detail_normal)) == SMB_SUCCESS) {
+		result = smb_removefile(&smb, &file);
+		smb_freefilemem(&file);
 	}
-	close(file);
-	if(f->dir==cfg->user_dir)  /* remove file from index */
-		rmuserxfers(cfg,0,0,f->name);
-	return(TRUE);
+	smb_close(&smb);
+	return result == SMB_SUCCESS;
 }
 
 /****************************************************************************/
-/* Checks  directory data file for 'filename' (must be padded). If found,   */
-/* it returns the 1, else returns 0.                                        */
-/* Called from upload and bulkupload                                        */
+/* Returns full (case-corrected) path to specified file						*/
+/* 'path' should be MAX_PATH + 1 chars in size								*/
+/* If the directory is configured to allow file-checking and the file does	*/
+/* not exist, the size element is set to -1.								*/
 /****************************************************************************/
-BOOL DLLCALL findfile(scfg_t* cfg, uint dirnum, char *filename)
-{
-	char str[MAX_PATH+1],fname[13],*ixbbuf;
-    int i,file;
-    long length,l;
-
-	SAFECOPY(fname,filename);
-	strupr(fname);
-	for(i=8;i<12;i++)   /* Turn FILENAME.EXT into FILENAMEEXT */
-		fname[i]=fname[i+1];
-	SAFEPRINTF2(str,"%s%s.ixb",cfg->dir[dirnum]->data_dir,cfg->dir[dirnum]->code);
-	if((file=sopen(str,O_RDONLY|O_BINARY,SH_DENYWR))==-1) return(FALSE);
-	length=(long)filelength(file);
-	if(!length) {
-		close(file);
-		return(FALSE); 
+char* getfilepath(scfg_t* cfg, file_t* f, char* path)
+{
+	bool fchk = true;
+	const char* name = f->name == NULL ? f->file_idx.name : f->name;
+	if(f->dir >= cfg->total_dirs)
+		safe_snprintf(path, MAX_PATH, "%s%s", cfg->temp_dir, name);
+	else {
+		safe_snprintf(path, MAX_PATH, "%s%s", cfg->dir[f->dir]->path, name);
+		fchk = (cfg->dir[f->dir]->misc & DIR_FCHK) != 0;
 	}
-	if((ixbbuf=(char *)malloc(length))==NULL) {
-		close(file);
-		return(FALSE); 
+	if(f->size == 0 && fchk && !fexistcase(path))
+		f->size = -1;
+	return path;
+}
+
+off_t getfilesize(scfg_t* cfg, file_t* f)
+{
+	char fpath[MAX_PATH + 1];
+	if(f->size > 0)
+		return f->size;
+	f->size = flength(getfilepath(cfg, f, fpath));
+	return f->size;
+}
+
+time_t getfiletime(scfg_t* cfg, file_t* f)
+{
+	char fpath[MAX_PATH + 1];
+	if(f->time > 0)
+		return f->time;
+	f->time = fdate(getfilepath(cfg, f, fpath));
+	return f->time;
+}
+
+ulong gettimetodl(scfg_t* cfg, file_t* f, uint rate_cps)
+{
+	if(getfilesize(cfg, f) < 1)
+		return 0;
+	if(f->size <= (off_t)rate_cps)
+		return 1;
+	return (ulong)(f->size / rate_cps);
+}
+
+bool hashfile(scfg_t* cfg, file_t* f)
+{
+	bool result = false;
+	smb_t smb;
+
+	if(cfg->dir[f->dir]->misc & DIR_NOHASH)
+		return false;
+
+	if(smb_open_dir(cfg, &smb, f->dir) != SMB_SUCCESS)
+		return false;
+
+	if(!(smb.status.attr & SMB_NOHASH)) {
+		char fpath[MAX_PATH + 1];
+		getfilepath(cfg, f, fpath);
+		if((f->file_idx.hash.flags = smb_hashfile(fpath, getfilesize(cfg, f), &f->file_idx.hash.data)) != 0)
+			result = true;
+	}
+	smb_close(&smb);
+	return result;
+}
+
+bool addfile(scfg_t* cfg, uint dirnum, file_t* f, const char* extdesc)
+{
+	char fpath[MAX_PATH + 1];
+	smb_t smb;
+
+	if(smb_open_dir(cfg, &smb, dirnum) != SMB_SUCCESS)
+		return false;
+
+	getfilepath(cfg, f, fpath);
+	int result = smb_addfile(&smb, f, SMB_SELFPACK, extdesc, fpath);
+	smb_close(&smb);
+	return result == SMB_SUCCESS;
+}
+
+/* 'size' does not include the NUL-terminator */
+char* format_filename(const char* fname, char* buf, size_t size, bool pad)
+{
+	size_t fnlen = strlen(fname);
+	char* ext = getfext(fname);
+	if(ext != NULL) {
+		size_t extlen = strlen(ext);
+		if(extlen >= size)
+			safe_snprintf(buf, size + 1, "%s", fname);
+		else {
+			fnlen -= extlen;
+			if(fnlen > size - extlen)
+				fnlen = size - extlen;
+			safe_snprintf(buf, size + 1, "%-*.*s%s", (int)(pad ? (size - extlen) : 0), (int)fnlen, fname, ext);
+		}
+	} else	/* no extension */
+		snprintf(buf, size + 1, "%s", fname);
+	return buf;
+}
+
+int archive_type(const char* archive, char* str, size_t size)
+{
+	int result;
+	struct archive *ar;
+	struct archive_entry *entry;
+
+	if((ar = archive_read_new()) == NULL) {
+		safe_snprintf(str, size, "archive_read_new returned NULL");
+		return -1;
 	}
-	if(read(file,ixbbuf,length)!=length) {
-		close(file);
-		free(ixbbuf);
-		return(FALSE); 
+	archive_read_support_filter_all(ar);
+	archive_read_support_format_all(ar);
+	if((result = archive_read_open_filename(ar, archive, 10240)) != ARCHIVE_OK) {
+		safe_snprintf(str, size, "archive_read_open_filename returned %d: %s"
+			,result, archive_error_string(ar));
+		archive_read_free(ar);
+		return result;
 	}
-	close(file);
-	for(l=0;l<length;l+=F_IXBSIZE) {
-		for(i=0;i<11;i++)
-			if(toupper(fname[i])!=toupper(ixbbuf[l+i])) break;
-		if(i==11) break; 
+	result = archive_filter_code(ar, 0);
+	if(result >= 0) {
+		int comp = result;
+		result = archive_read_next_header(ar, &entry);
+		if(result != ARCHIVE_OK)
+			safe_snprintf(str, size, "archive_read_next_header returned %d: %s"
+				,result, archive_error_string(ar));
+		else {
+			result = archive_format(ar);
+			if(comp > 0)
+				safe_snprintf(str, size, "%s/%s", archive_filter_name(ar, 0), archive_format_name(ar));
+			else
+				safe_snprintf(str, size, "%s", archive_format_name(ar));
+		}
 	}
-	free(ixbbuf);
-	if(l!=length)
-		return(TRUE);
-	return(FALSE);
+	archive_read_free(ar);
+	return result;
 }
 
-/****************************************************************************/
-/* Turns FILE.EXT into FILE    .EXT                                         */
-/****************************************************************************/
-char* DLLCALL padfname(const char *filename, char *str)
-{
-    int c,d;
-
-	for(c=0;c<8;c++)
-		if(filename[c]=='.' || !filename[c]) break;
-		else str[c]=filename[c];
-	d=c;
-	if(filename[c]=='.') c++;
-	while(d<8)
-		str[d++]=' ';
-	if(filename[c]>' ')	/* Change "FILE" to "FILE        " */
-		str[d++]='.';	/* (don't add a dot if there's no extension) */
-	else
-		str[d++]=' ';
-	while(d<12)
-		if(!filename[c]) break;
-		else str[d++]=filename[c++];
-	while(d<12)
-		str[d++]=' ';
-	str[d]=0;
-	return(str);
+str_list_t directory(const char* path)
+{
+	int			flags = GLOB_MARK;
+	glob_t		g;
+
+	if(glob(path, flags, NULL, &g) != 0)
+		return NULL;
+	str_list_t list = strListInit();
+	for(size_t i = 0; i < g.gl_pathc; i++) {
+		strListPush(&list, g.gl_pathv[i]);
+	}
+	globfree(&g);
+	return list;
 }
 
-/****************************************************************************/
-/* Turns FILE    .EXT into FILE.EXT                                         */
-/****************************************************************************/
-char* DLLCALL unpadfname(const char *filename, char *str)
+const char* supported_archive_formats[] = { "zip", "7z", "tgz", "tbz", NULL };
+// Returns negative on error
+long create_archive(const char* archive, const char* format
+	,bool with_path, str_list_t file_list, char* error, size_t maxerrlen)
 {
-    int c,d;
+	int result;
+	struct archive *ar;
 
-	for(c=0,d=0;filename[c];c++)
-		if(filename[c]!=' ') str[d++]=filename[c];
-	str[d]=0;
-	return(str);
-}
+	if(file_list == NULL || *file_list == NULL)
+		return 0;
 
-/****************************************************************************/
-/* Removes any files in the user transfer index (XFER.IXT) that match the   */
-/* specifications of dest, or source user, or filename or any combination.  */
-/****************************************************************************/
-BOOL DLLCALL rmuserxfers(scfg_t* cfg, int fromuser, int destuser, char *fname)
-{
-    char str[MAX_PATH+1],*ixtbuf;
-    int file;
-    long l,length;
-
-	SAFEPRINTF(str,"%sxfer.ixt", cfg->data_dir);
-	if(!fexist(str))
-		return(FALSE);
-	if(!flength(str)) {
-		remove(str);
-		return(FALSE); 
+	if((ar = archive_write_new()) == NULL) {
+		safe_snprintf(error, maxerrlen, "archive_write_new returned NULL");
+		return -1;
 	}
-	if((file=sopen(str,O_RDONLY|O_BINARY,SH_DENYWR))==-1) {
-		return(FALSE); 
+	if(stricmp(format, "tgz") == 0) {
+		archive_write_add_filter_gzip(ar);
+		archive_write_set_format_pax_restricted(ar);
+	} else if(stricmp(format, "tbz") == 0) {
+		archive_write_add_filter_bzip2(ar);
+		archive_write_set_format_pax_restricted(ar);
+	} else if(stricmp(format, "zip") == 0) {
+		archive_write_set_format_zip(ar);
+	} else if(stricmp(format, "7z") == 0) {
+		archive_write_set_format_7zip(ar);
+	} else {
+		safe_snprintf(error, maxerrlen, "unsupported format: %s", format);
+		return -2;
 	}
-	length=(long)filelength(file);
-	if((ixtbuf=(char *)malloc(length))==NULL) {
-		close(file);
-		return(FALSE); 
+	if((result = archive_write_open_filename(ar, archive)) != ARCHIVE_OK) {
+		safe_snprintf(error, maxerrlen, "archive_write_open_filename(%s) returned %d: %s"
+			,archive, result, archive_error_string(ar));
+		archive_write_free(ar);
+		return result;
 	}
-	if(read(file,ixtbuf,length)!=length) {
-		close(file);
-		free(ixtbuf);
-		return(FALSE); 
+	ulong file_count = 0;
+	for(;file_list[file_count] != NULL; file_count++) {
+		struct archive_entry* entry;
+		struct stat st;
+		const char* filename = file_list[file_count];
+		FILE* fp = fopen(filename, "rb");
+		if(fp == NULL) {
+			safe_snprintf(error, maxerrlen, "%d opening %s", errno, filename);
+			break;
+		}
+		fstat(fileno(fp), &st);
+		if((entry = archive_entry_new()) == NULL) {
+			safe_snprintf(error, maxerrlen, "archive_entry_new returned NULL");
+			fclose(fp);
+			break;
+		}
+		if(with_path)
+			archive_entry_set_pathname(entry, filename);
+		else
+			archive_entry_set_pathname(entry, getfname(filename));
+		archive_entry_set_size(entry, st.st_size);
+		archive_entry_set_mtime(entry, st.st_mtime, 0);
+		archive_entry_set_filetype(entry, AE_IFREG);
+		archive_entry_set_perm(entry, 0644);
+		if((result = archive_write_header(ar, entry)) != ARCHIVE_OK)
+			safe_snprintf(error, maxerrlen, "archive_write_header returned %d", result);
+		else while(!feof(fp)) {
+			char buf[256 * 1024];
+			size_t len = fread(buf, 1, sizeof(buf), fp);
+			if((result = archive_write_data(ar, buf, len)) != len) {
+				safe_snprintf(error, maxerrlen, "archive_write_data returned %d instead of %d", result, (int)len);
+				break;
+			} else
+				result = ARCHIVE_OK;
+		}
+		fclose(fp);
+		archive_entry_free(entry);
+		if(result != ARCHIVE_OK)
+			break;
 	}
-	close(file);
-	if((file=sopen(str,O_WRONLY|O_TRUNC|O_BINARY,SH_DENYRW))==-1) {
-		free(ixtbuf);
-		return(FALSE); 
+	archive_write_close(ar);
+	archive_write_free(ar);
+	if(file_list[file_count] != NULL)
+		return result < 0 ? result : -1;
+	return file_count;
+}
+
+long extract_files_from_archive(const char* archive, const char* outdir, const char* allowed_filename_chars
+	,bool with_path, long max_files, str_list_t file_list, char* error, size_t maxerrlen)
+{
+	int result;
+	struct archive *ar;
+	struct archive_entry *entry;
+	long extracted = 0;
+	char fpath[MAX_PATH + 1];
+
+	if(error != NULL && maxerrlen >= 1)
+		*error = '\0';
+	if((ar = archive_read_new()) == NULL) {
+		safe_snprintf(error, maxerrlen, "archive_read_new returned NULL");
+		return -1;
 	}
-	for(l=0;l<length;l+=24) {
-		if(fname!=NULL && fname[0]) {               /* fname specified */
-			if(!strncmp(ixtbuf+l+5,fname,12)) {     /* this is the file */
-				if(destuser && fromuser) {          /* both dest and from user */
-					if(atoi(ixtbuf+l)==destuser && atoi(ixtbuf+l+18)==fromuser)
-						continue;                   /* both match */
-				}
-				else if(fromuser) {                 /* from user */
-					if(atoi(ixtbuf+l+18)==fromuser) /* matches */
-						continue; 
-				}
-				else if(destuser) {                 /* dest user */
-					if(atoi(ixtbuf+l)==destuser)    /* matches */
-						continue; 
-				}
-				else continue;		                /* no users, so match */
-			}
-		}
-		else if(destuser && fromuser) {
-			if(atoi(ixtbuf+l+18)==fromuser && atoi(ixtbuf+l)==destuser)
-				continue; 
+	archive_read_support_filter_all(ar);
+	archive_read_support_format_all(ar);
+	if((result = archive_read_open_filename(ar, archive, 10240)) != ARCHIVE_OK) {
+		safe_snprintf(error, maxerrlen, "archive_read_open_filename returned %d: %s"
+			,result, archive_error_string(ar));
+		archive_read_free(ar);
+		return result >= 0 ? -1 : result;
+	}
+	while(1) {
+		result = archive_read_next_header(ar, &entry);
+		if(result != ARCHIVE_OK) {
+			if(result != ARCHIVE_EOF)
+				safe_snprintf(error, maxerrlen, "archive_read_next_header returned %d: %s"
+					,result, archive_error_string(ar));
+			break;
 		}
-		else if(destuser && atoi(ixtbuf+l)==destuser)
+		const char* pathname = archive_entry_pathname(entry);
+		if(pathname == NULL)
 			continue;
-		else if(fromuser && atoi(ixtbuf+l+18)==fromuser)
+		int filetype = archive_entry_filetype(entry);
+		if(filetype == AE_IFDIR) {
+			if(!with_path)
+				continue;
+			if(strstr(pathname, "..") != NULL) {
+				safe_snprintf(error, maxerrlen, "Illegal double-dots in path '%s'", pathname);
+				break;
+			}
+			SAFECOPY(fpath, outdir);
+			backslash(fpath);
+			SAFECAT(fpath, pathname);
+			if(mkpath(fpath) != 0) {
+				char err[256];
+				safe_snprintf(error, maxerrlen, "%d (%s) creating path '%s'", errno, safe_strerror(errno, err, sizeof(err)), fpath);
+				break;
+			}
 			continue;
-		write(file,ixtbuf+l,24); 
-	}
-	close(file);
-	free(ixtbuf);
+		}
+		if(filetype != AE_IFREG)
+			continue;
+		char* filename = getfname(pathname);
+		if(allowed_filename_chars != NULL
+			&& *allowed_filename_chars != '\0'
+			&& strspn(filename, allowed_filename_chars) != strlen(filename)) {
+			safe_snprintf(error, maxerrlen, "disallowed filename '%s'", pathname);
+			break;
+		}
+		if(!with_path)
+			pathname = filename;
+		if(file_list != NULL) {
+			int i;
+			for (i = 0; file_list[i] != NULL; i++)
+				if(wildmatch(pathname, file_list[i], with_path, /* case-sensitive: */false))
+					break;
+			if(file_list[i] == NULL)
+				continue;
+		}
+		SAFECOPY(fpath, outdir);
+		backslash(fpath);
+		SAFECAT(fpath, pathname);
+		FILE* fp = fopen(fpath, "wb");
+		if(fp == NULL) {
+			char err[256];
+			safe_snprintf(error, maxerrlen, "%d (%s) opening/creating '%s'", errno, safe_strerror(errno, err, sizeof(err)), fpath);
+			break;
+		}
+
+		const void *buff;
+		size_t size;
+		la_int64_t offset;
 
-	return(TRUE);
+		for(;;) {
+			result = archive_read_data_block(ar, &buff, &size, &offset);
+			if(result == ARCHIVE_EOF) {
+				extracted++;
+				break;
+			}
+			if(result < ARCHIVE_OK) {
+				safe_snprintf(error, maxerrlen, "archive_read_data_block returned %d: %s"
+					,result, archive_error_string(ar));
+				break;
+			}
+			if(fwrite(buff, 1, size, fp) != size)
+				break;
+		}
+		fclose(fp);
+		if(result != ARCHIVE_EOF)
+			(void)remove(fpath);
+		if(max_files && extracted >= max_files) {
+			safe_snprintf(error, maxerrlen, "maximum number of files (%lu) extracted", max_files);
+			break;
+		}
+	}
+	archive_read_free(ar);
+	return extracted;
 }
 
-void DLLCALL getextdesc(scfg_t* cfg, uint dirnum, ulong datoffset, char *ext)
+bool extract_diz(scfg_t* cfg, file_t* f, str_list_t diz_fnames, char* path, size_t maxlen)
 {
-	char str[MAX_PATH+1];
-	int file;
+	int i;
+	char archive[MAX_PATH + 1];
+	char* default_diz_fnames[] = { "FILE_ID.DIZ", "DESC.SDI", NULL };
+
+	getfilepath(cfg, f, archive);
+	if(diz_fnames == NULL)
+		diz_fnames = default_diz_fnames;
+
+	if(!fexistcase(archive))
+		return false;
+
+	for(i = 0; diz_fnames[i] != NULL; i++) {
+		safe_snprintf(path, maxlen, "%s%s", cfg->temp_dir, diz_fnames[i]); // no slash
+		removecase(path);
+		if(fexistcase(path))	// failed to delete?
+			return false;
+	}
+
+	if(extract_files_from_archive(archive
+		,/* outdir: */cfg->temp_dir
+		,/* allowed_filename_chars: */NULL /* any */
+		,/* with_path: */false
+		,/* max_files: */strListCount(diz_fnames)
+		,/* file_list: */diz_fnames
+		,/* error: */NULL, 0) >= 0) {
+		for(i = 0; diz_fnames[i] != NULL; i++) {
+			safe_snprintf(path, maxlen, "%s%s", cfg->temp_dir, diz_fnames[i]); // no slash
+			if(fexistcase(path))
+				return true;
+		}
+		return false;
+	}
+
+	char* fext = getfext(f->name);
 
-	memset(ext,0,F_EXBSIZE+1);
-	SAFEPRINTF2(str,"%s%s.exb",cfg->dir[dirnum]->data_dir,cfg->dir[dirnum]->code);
-	if((file=nopen(str,O_RDONLY))==-1)
-		return;
-	lseek(file,(datoffset/F_LEN)*F_EXBSIZE,SEEK_SET);
-	read(file,ext,F_EXBSIZE);
-	close(file);
+	if(fext == NULL)
+		return false;
+
+	for(i = 0; i < cfg->total_fextrs; i++)
+		if(stricmp(cfg->fextr[i]->ext, fext + 1) == 0 && chk_ar(cfg, cfg->fextr[i]->ar, /* user: */NULL, /* client: */NULL))
+			break;
+	if(i >= cfg->total_fextrs)
+		return false;
+
+	fextr_t* fextr = cfg->fextr[i];
+	char cmd[512];
+	for(i = 0; diz_fnames[i] != NULL; i++) {
+		safe_snprintf(path, maxlen, "%s%s", cfg->temp_dir, diz_fnames[i]);
+		system(cmdstr(cfg, /* user: */NULL, fextr->cmd, archive, diz_fnames[i], cmd, sizeof(cmd)));
+		if(fexistcase(path))
+			return true;
+	}
+	return false;
 }
 
-void DLLCALL putextdesc(scfg_t* cfg, uint dirnum, ulong datoffset, char *ext)
+str_list_t read_diz(const char* path, size_t max_line_len)
 {
-	char str[MAX_PATH+1],nulbuf[F_EXBSIZE];
-	int file;
+	FILE* fp = fopen(path, "r");
+	if(fp == NULL)
+		return NULL;
 
-	strip_ansi(ext);
-	strip_invalid_attr(ext);	/* eliminate bogus ctrl-a codes */
-	memset(nulbuf,0,sizeof(nulbuf));
-	SAFEPRINTF2(str,"%s%s.exb",cfg->dir[dirnum]->data_dir,cfg->dir[dirnum]->code);
-	if((file=nopen(str,O_WRONLY|O_CREAT))==-1)
-		return;
-	lseek(file,0L,SEEK_END);
-	while(filelength(file)<(long)(datoffset/F_LEN)*F_EXBSIZE)
-		write(file,nulbuf,sizeof(nulbuf));
-	lseek(file,(datoffset/F_LEN)*F_EXBSIZE,SEEK_SET);
-	write(file,ext,F_EXBSIZE);
-	close(file);
+	str_list_t lines = strListReadFile(fp, NULL, max_line_len);
+	fclose(fp);
+	return lines;
 }
 
-/****************************************************************************/
-/* Update the upload date for the file 'f'                                  */
-/****************************************************************************/
-int DLLCALL update_uldate(scfg_t* cfg, file_t* f)
-{
-	char str[MAX_PATH+1],fname[13];
-	int i,file;
-	long l,length;
-
-	/*******************/
-	/* Update IXB File */
-	/*******************/
-	SAFEPRINTF2(str,"%s%s.ixb",cfg->dir[f->dir]->data_dir,cfg->dir[f->dir]->code);
-	if((file=nopen(str,O_RDWR))==-1)
-		return(errno); 
-	length=(long)filelength(file);
-	if(length%F_IXBSIZE) {
-		close(file);
-		return(-1); 
-	}
-	SAFECOPY(fname,f->name);
-	for(i=8;i<12;i++)   /* Turn FILENAME.EXT into FILENAMEEXT */
-		fname[i]=fname[i+1];
-	for(l=0;l<length;l+=F_IXBSIZE) {
-		read(file,str,F_IXBSIZE);      /* Look for the filename in the IXB file */
-		str[11]=0;
-		if(!stricmp(fname,str)) break; 
+char* format_diz(str_list_t lines, char* str, size_t maxlen, bool allow_ansi)
+{
+	if(lines == NULL) {
+		*str = '\0';
+		return NULL;
 	}
-	if(l>=length) {
-		close(file);
-		return(-2); 
+	strListTruncateTrailingWhitespaces(lines);
+	if(!allow_ansi) {
+		for(size_t i = 0; lines[i] != NULL; i++) {
+			strip_ansi(lines[i]);
+			strip_ctrl(lines[i], lines[i]);
+		}
 	}
-	lseek(file,l+14,SEEK_SET);
-	write(file,&f->dateuled,4);
-	close(file);
+	strListFastDeleteBlanks(lines);
+	return strListCombine(lines, str, maxlen, "\r\n");
+}
 
-	/*******************************************/
-	/* Update last upload date/time stamp file */
-	/*******************************************/
-	SAFEPRINTF2(str,"%s%s.dab",cfg->dir[f->dir]->data_dir,cfg->dir[f->dir]->code);
-	if((file=nopen(str,O_WRONLY|O_CREAT))==-1)
-		return(errno);
+// Take a verbose extended description (e.g. FILE_ID.DIZ)
+// and convert to suitable short description
+char* prep_file_desc(const char *src, char* dest)
+{
+	int out;
+
+	FIND_ALPHANUMERIC(src);
+	for(out = 0; *src != '\0' && out < LEN_FDESC; src++) {
+		if(IS_WHITESPACE(*src) && out && IS_WHITESPACE(dest[out - 1]))
+			continue;
+		if(!IS_ALPHANUMERIC(*src) && out && *src == dest[out - 1])
+			continue;
+		if(*src == '\n') {
+			if(out && !IS_WHITESPACE(dest[out - 1]))
+				dest[out++] = ' ';
+			continue;
+		}
+		if(IS_CONTROL(*src))
+			continue;
+		dest[out++] = *src;
+	}
+	dest[out] = '\0';
+	return dest;
+}
 
-	write(file,&f->dateuled,4);
-	close(file); 
-	return(0);
+static const char* quoted_string(const char* str, char* buf, size_t maxlen)
+{
+	if(strchr(str,' ')==NULL)
+		return(str);
+	safe_snprintf(buf,maxlen,"\"%s\"",str);
+	return(buf);
 }
 
+#define QUOTED_STRING(ch, str, buf, maxlen) \
+	((IS_ALPHA(ch) && IS_UPPERCASE(ch)) ? str : quoted_string(str,buf,maxlen))
+
 /****************************************************************************/
-/* Returns full (case-corrected) path to specified file						*/
+/* Returns command line generated from instr with %c replacements           */
+/* This is the C-exported version of sbbs_t::cmdstr()						*/
 /****************************************************************************/
-char* DLLCALL getfilepath(scfg_t* cfg, file_t* f, char* path)
+char* cmdstr(scfg_t* cfg, user_t* user, const char* instr, const char* fpath
+						,const char* fspec, char* cmd, size_t maxlen)
 {
-	char	fname[MAX_PATH+1];
+	char	str[MAX_PATH+1];
+    int		i,j,len;
+
+	len=strlen(instr);
+	for(i=j=0; i < len && j < (int)maxlen; i++) {
+        if(instr[i]=='%') {
+            i++;
+            cmd[j]=0;
+			int avail = maxlen - j;
+			char ch=instr[i];
+			if(IS_ALPHA(ch))
+				ch=toupper(ch);
+            switch(ch) {
+                case 'A':   /* User alias */
+					if(user!=NULL)
+						strncat(cmd,QUOTED_STRING(instr[i],user->alias,str,sizeof(str)), avail);
+                    break;
+                case 'B':   /* Baud (DTE) Rate */
+                    break;
+                case 'C':   /* Connect Description */
+					if(user!=NULL)
+						strncat(cmd,user->modem, avail);
+                    break;
+                case 'D':   /* Connect (DCE) Rate */
+                    break;
+                case 'E':   /* Estimated Rate */
+                    break;
+                case 'F':   /* File path */
+                    strncat(cmd,QUOTED_STRING(instr[i],fpath,str,sizeof(str)), avail);
+                    break;
+                case 'G':   /* Temp directory */
+                    strncat(cmd,cfg->temp_dir, avail);
+                    break;
+                case 'H':   /* Port Handle or Hardware Flow Control */
+                    break;
+                case 'I':   /* IP address */
+					if(user!=NULL)
+						strncat(cmd,user->note, avail);
+                    break;
+                case 'J':
+                    strncat(cmd,cfg->data_dir, avail);
+                    break;
+                case 'K':
+                    strncat(cmd,cfg->ctrl_dir, avail);
+                    break;
+                case 'L':   /* Lines per message */
+					if(user!=NULL)
+						strncat(cmd,ultoa(cfg->level_linespermsg[user->level],str,10), avail);
+                    break;
+                case 'M':   /* Minutes (credits) for user */
+					if(user!=NULL)
+						strncat(cmd,ultoa(user->min,str,10), avail);
+                    break;
+                case 'N':   /* Node Directory (same as SBBSNODE environment var) */
+                    strncat(cmd,cfg->node_dir, avail);
+                    break;
+                case 'O':   /* SysOp */
+                    strncat(cmd,QUOTED_STRING(instr[i],cfg->sys_op,str,sizeof(str)), avail);
+                    break;
+                case 'P':   /* Client protocol */
+                    break;
+                case 'Q':   /* QWK ID */
+                    strncat(cmd,cfg->sys_id, avail);
+                    break;
+                case 'R':   /* Rows */
+					if(user!=NULL)
+						strncat(cmd,ultoa(user->rows,str,10), avail);
+                    break;
+                case 'S':   /* File Spec */
+                    strncat(cmd, fspec, avail);
+                    break;
+                case 'T':   /* Time left in seconds */
+                    break;
+                case 'U':   /* UART I/O Address (in hex) */
+                    strncat(cmd,ultoa(cfg->com_base,str,16), avail);
+                    break;
+                case 'V':   /* Synchronet Version */
+                    sprintf(str,"%s%c",VERSION,REVISION);
+					strncat(cmd,str, avail);
+                    break;
+                case 'W':   /* Columns/width */
+                    break;
+                case 'X':
+					if(user!=NULL)
+						strncat(cmd,cfg->shell[user->shell]->code, avail);
+                    break;
+                case '&':   /* Address of msr */
+                    break;
+                case 'Y':
+                    break;
+                case 'Z':
+                    strncat(cmd,cfg->text_dir, avail);
+                    break;
+				case '~':	/* DOS-compatible (8.3) filename */
+				{
+#ifdef _WIN32
+					char sfpath[MAX_PATH+1];
+					SAFECOPY(sfpath,fpath);
+					GetShortPathName(fpath,sfpath,sizeof(sfpath));
+					strncat(cmd,sfpath, avail);
+#else
+                    strncat(cmd,QUOTED_STRING(instr[i],fpath,str,sizeof(str)), avail);
+#endif
+					break;
+				}
+                case '!':   /* EXEC Directory */
+                    strncat(cmd,cfg->exec_dir, avail);
+                    break;
+                case '@':   /* EXEC Directory for DOS/OS2/Win32, blank for Unix */
+#ifndef __unix__
+                    strncat(cmd,cfg->exec_dir, avail);
+#endif
+                    break;
+
+                case '#':   /* Node number (same as SBBSNNUM environment var) */
+                    sprintf(str,"%d",cfg->node_num);
+                    strncat(cmd,str, avail);
+                    break;
+                case '*':
+                    sprintf(str,"%03d",cfg->node_num);
+                    strncat(cmd,str, avail);
+                    break;
+                case '$':   /* Credits */
+					if(user!=NULL)
+						strncat(cmd,ultoa(user->cdt+user->freecdt,str,10), avail);
+                    break;
+                case '%':   /* %% for percent sign */
+                    strncat(cmd,"%", avail);
+                    break;
+				case '.':	/* .exe for DOS/OS2/Win32, blank for Unix */
+#ifndef __unix__
+					strncat(cmd,".exe", avail);
+#endif
+					break;
+				case '?':	/* Platform */
+#ifdef __OS2__
+					strcpy(str,"OS2");
+#else
+					strcpy(str,PLATFORM_DESC);
+#endif
+					strlwr(str);
+					strncat(cmd,str, avail);
+					break;
+				case '^':	/* Architecture */
+					strncat(cmd, ARCHITECTURE_DESC, avail);
+					break;
+                default:    /* unknown specification */
+                    if(IS_DIGIT(instr[i]) && user!=NULL) {
+                        sprintf(str,"%0*d",instr[i]&0xf,user->number);
+                        strncat(cmd,str, avail);
+					}
+                    break;
+			}
+            j=strlen(cmd);
+		}
+        else
+            cmd[j++]=instr[i];
+	}
+    cmd[j]=0;
 
-	unpadfname(f->name,fname);
-	if(f->dir>=cfg->total_dirs)
-		safe_snprintf(path,MAX_PATH,"%s%s",cfg->temp_dir,fname);
-	else
-		safe_snprintf(path,MAX_PATH,"%s%s",f->altpath>0 && f->altpath<=cfg->altpaths 
-			? cfg->altpath[f->altpath-1] : cfg->dir[f->dir]->path
-			,fname);
-	fexistcase(path);
-	return(path);
+    return(cmd);
 }
+
diff --git a/src/sbbs3/filedat.h b/src/sbbs3/filedat.h
index 67f3d3e8372b044b45a628860a4613dfb2cb3bae..277d9ad56c4ccb594a85352368c3e970384de1e5 100644
--- a/src/sbbs3/filedat.h
+++ b/src/sbbs3/filedat.h
@@ -1,4 +1,4 @@
-/* Synchronet Filebase Access functions */
+/* Synchronet FileBase Access functions */
 
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
@@ -24,29 +24,66 @@
 
 #include "scfgdefs.h"	// scfg_t
 #include "dllexport.h"
+#include "smblib.h"
+
+#include <stdbool.h>
 
 #ifdef __cplusplus
 extern "C" {
 #endif
 
-DLLEXPORT BOOL		getfileixb(scfg_t* cfg, file_t* f);
-DLLEXPORT BOOL		putfileixb(scfg_t* cfg, file_t* f);
-DLLEXPORT BOOL		getfiledat(scfg_t* cfg, file_t* f);
-DLLEXPORT BOOL		putfiledat(scfg_t* cfg, file_t* f);
-DLLEXPORT void		putextdesc(scfg_t* cfg, uint dirnum, ulong datoffset, char *ext);
-DLLEXPORT void		getextdesc(scfg_t* cfg, uint dirnum, ulong datoffset, char *ext);
-DLLEXPORT char*		getfilepath(scfg_t* cfg, file_t* f, char* path);
+DLLEXPORT bool			newfiles(smb_t*, time_t);
+DLLEXPORT time_t		newfiletime(smb_t*);
+DLLEXPORT bool			update_newfiletime(smb_t*, time_t);
+DLLEXPORT time_t		dir_newfiletime(scfg_t*, uint dirnum);
+DLLEXPORT time_t		lastfiletime(smb_t*); // Reads the last index record
+
+DLLEXPORT bool			findfile(scfg_t* cfg, uint dirnum, const char *filename, file_t*);
+DLLEXPORT bool			loadfile(scfg_t*, uint dirnum, const char* filename, file_t*, enum file_detail);
+DLLEXPORT file_t*	loadfiles(smb_t*, const char* filespec, time_t, enum file_detail, enum file_sort, size_t* count);
+DLLEXPORT void			sortfiles(file_t*, size_t count, enum file_sort);
+DLLEXPORT void			freefiles(file_t*, size_t count);
+DLLEXPORT str_list_t	loadfilenames(smb_t*, const char* filespec, time_t t, enum file_sort, size_t* count);
+DLLEXPORT void			sortfilenames(str_list_t, size_t count, enum file_sort);
+DLLEXPORT bool			updatefile(scfg_t*, file_t*);
+DLLEXPORT char*			getfilepath(scfg_t*, file_t*, char* path);
+DLLEXPORT off_t			getfilesize(scfg_t*, file_t*);
+DLLEXPORT time_t		getfiletime(scfg_t*, file_t*);
+DLLEXPORT ulong			gettimetodl(scfg_t*, file_t*, uint rate_cps);
+DLLEXPORT bool			hashfile(scfg_t*, file_t*);
+DLLEXPORT bool			addfile(scfg_t*, uint dirnum, file_t*, const char* extdesc);
+DLLEXPORT bool			removefile(scfg_t*, uint dirnum, const char* filename);
+DLLEXPORT char*			format_filename(const char* fname, char* buf, size_t, bool pad);
+DLLEXPORT bool			extract_diz(scfg_t*, file_t*, str_list_t diz_fname, char* path, size_t);
+DLLEXPORT str_list_t	read_diz(const char* path, size_t max_line_len);
+DLLEXPORT char*			format_diz(str_list_t lines, char*, size_t maxlen, bool allow_ansi);
+DLLEXPORT char*			prep_file_desc(const char *src, char* dst);
+
+DLLEXPORT str_list_t	directory(const char* path);
+DLLEXPORT long			create_archive(const char* archive, const char* format
+						               ,bool with_path, str_list_t file_list, char* error, size_t maxerrlen);
+DLLEXPORT char*			cmdstr(scfg_t*, user_t*, const char* instr, const char* fpath, const char* fspec, char* cmd, size_t);
+DLLEXPORT long			extract_files_from_archive(const char* archive, const char* outdir, const char* allowed_filename_chars
+						                           ,bool with_path, long max_files, str_list_t file_list, char* error, size_t);
+DLLEXPORT int			archive_type(const char* archive, char* str, size_t size);
+extern const char*		supported_archive_formats[];
 
-DLLEXPORT BOOL		removefiledat(scfg_t* cfg, file_t* f);
-DLLEXPORT BOOL		addfiledat(scfg_t* cfg, file_t* f);
-DLLEXPORT BOOL		findfile(scfg_t* cfg, uint dirnum, char *filename);
-DLLEXPORT char *	padfname(const char *filename, char *str);
-DLLEXPORT char *	unpadfname(const char *filename, char *str);
-DLLEXPORT BOOL		rmuserxfers(scfg_t* cfg, int fromuser, int destuser, char *fname);
+/* Batch file transfer queues */
+DLLEXPORT char*			batch_list_name(scfg_t* , uint usernumber, enum XFER_TYPE, char* fname, size_t);
+DLLEXPORT FILE*			batch_list_open(scfg_t* , uint usernumber, enum XFER_TYPE, bool create);
+DLLEXPORT str_list_t	batch_list_read(scfg_t* , uint usernumber, enum XFER_TYPE);
+DLLEXPORT bool			batch_list_write(scfg_t*, uint usernumber, enum XFER_TYPE, str_list_t list);
+DLLEXPORT bool			batch_list_clear(scfg_t*, uint usernumber, enum XFER_TYPE);
 
-DLLEXPORT int		update_uldate(scfg_t* cfg, file_t* f);
+DLLEXPORT bool			batch_file_add(scfg_t*, uint usernumber, enum XFER_TYPE, file_t*);
+DLLEXPORT bool			batch_file_exists(scfg_t*, uint usernumber, enum XFER_TYPE, const char* filename);
+DLLEXPORT bool			batch_file_remove(scfg_t*, uint usernumber, enum XFER_TYPE, const char* filename);
+DLLEXPORT size_t		batch_file_count(scfg_t*, uint usernumber, enum XFER_TYPE);
+DLLEXPORT bool			batch_file_get(scfg_t*, str_list_t, const char* filename, file_t*);
+DLLEXPORT int			batch_file_dir(scfg_t*, str_list_t, const char* filename);
+DLLEXPORT bool			batch_file_load(scfg_t*, str_list_t, const char* filename, file_t*);
 
 #ifdef __cplusplus
 }
 #endif
-#endif /* Don't add anything after this line */
\ No newline at end of file
+#endif /* Don't add anything after this line */
diff --git a/src/sbbs3/filelist.c b/src/sbbs3/filelist.c
index 8d307e25e955eb7da2faf4fe107af9e63d9dca66..0a532fc4cde7cc72142d60cf9013a55342408601 100644
--- a/src/sbbs3/filelist.c
+++ b/src/sbbs3/filelist.c
@@ -28,9 +28,10 @@
 #include "nopen.h"
 #include "filedat.h"
 #include "dat_rec.h"
+#include "smblib.h"
 #include <stdarg.h>
 
-#define FILELIST_VER "3.15"
+#define FILELIST_VER "3.19"
 
 #define MAX_NOTS 25
 
@@ -67,11 +68,33 @@ void stripctrlz(char *str)
 	strcpy(str,tmp);
 }
 
+char* byteStr(unsigned long value)
+{
+	static char tmp[128];
+
+	if(value>=(1024*1024*1024))
+		sprintf(tmp, "%5.1fG", value/(1024.0*1024.0*1024.0));
+	else if(value>=(1024*1024))
+		sprintf(tmp, "%5.1fM", value/(1024.0*1024.0));
+	else if(value>=1024)
+		sprintf(tmp, "%5.1fK", value/1024.0); 
+	else
+		sprintf(tmp, "%5luB", value);
+	return tmp;
+}
+
+void fprint_extdesc(FILE* fp, char* ext_desc, int desc_off)
+{
+	for(char* line = strtok(ext_desc, "\n"); line != NULL; line = strtok(NULL, "\n")) {
+		truncsp(line);
+		fprintf(fp, "\n%*s %s", desc_off, "", line);
+	}
+}
 
 #define ALL 	(1L<<0)
 #define PAD 	(1L<<1)
 #define HDR 	(1L<<2)
-#define CDT_	(1L<<3)
+#define CREDITS	(1L<<3)
 #define EXT 	(1L<<4)
 #define ULN 	(1L<<5)
 #define ULD 	(1L<<6)
@@ -93,14 +116,13 @@ int main(int argc, char **argv)
 {
 	char	revision[16];
 	char	error[512];
-	char	str[256],fname[256],ext,not[MAX_NOTS][9];
-	uchar	*datbuf,*ixbbuf;
-	int 	i,j,file,dirnum,libnum,desc_off,lines,nots=0
-			,omode=O_WRONLY|O_CREAT|O_TRUNC;
-	ulong	l,m,n,cdt,misc=0,total_cdt=0,total_files=0,dir_files,datbuflen;
-	time32_t uld,dld,now;
+	char	*p,str[256],fname[256],ext,not[MAX_NOTS][9];
+	int 	i,j,dirnum,libnum,desc_off,lines,nots=0;
+	char*	omode="w";
+	char*	pattern=NULL;
+	ulong	m,cdt,misc=0,total_cdt=0,total_files=0,dir_files;
 	long	max_age=0;
-	FILE	*in,*out=NULL;
+	FILE*	out=NULL;
 
 	sscanf("$Revision: 1.22 $", "%*s %s", revision);
 
@@ -122,14 +144,15 @@ int main(int argc, char **argv)
 		printf("switches: -lib name All directories of specified library\n");
 		printf("          -not code Exclude specific directory\n");
 		printf("          -new days Include only new files in listing (days since upload)\n");
-		printf("          -cat      Concatenate to existing outfile\n");
+		printf("          -inc pattern Only list files matching 'pattern'\n");
+		printf("          -cat      Concatenate to existing 'outfile'\n");
 		printf("          -pad      Pad filename with spaces\n");
 		printf("          -hdr      Include directory headers\n");
 		printf("          -cdt      Include credit value\n");
 		printf("          -tot      Include credit totals\n");
 		printf("          -uln      Include uploader's name\n");
 		printf("          -uld      Include upload date\n");
-		printf("          -dfd      Include DOS file date\n");
+		printf("          -dfd      Include current file date\n");
 		printf("          -dld      Include download date\n");
 		printf("          -dls      Include total downloads\n");
 		printf("          -nod      Exclude normal descriptions\n");
@@ -143,7 +166,12 @@ int main(int argc, char **argv)
 		exit(0); 
 	}
 
-	now=time32(NULL);
+	p=getenv("SBBSCTRL");
+	if(p==NULL) {
+		printf("\nSBBSCTRL environment variable not set.\n");
+		printf("\nExample: SET SBBSCTRL=/sbbs/ctrl\n");
+		exit(1);
+	}
 
 	memset(&scfg,0,sizeof(scfg));
 	scfg.size=sizeof(scfg);
@@ -227,14 +255,22 @@ int main(int argc, char **argv)
 			}
 			max_age=strtol(argv[i],NULL,0);
 		}
+		else if(!stricmp(argv[i],"-inc")) {
+			i++;
+			if(i>=argc) {
+				printf("\nFilename pattern must follow -inc parameter.\n");
+				exit(1); 
+			}
+			pattern = argv[i];
+		}
 		else if(!stricmp(argv[i],"-pad"))
 			misc|=PAD;
 		else if(!stricmp(argv[i],"-cat"))
-			omode=O_WRONLY|O_CREAT|O_APPEND;
+			omode="a";
 		else if(!stricmp(argv[i],"-hdr"))
 			misc|=HDR;
 		else if(!stricmp(argv[i],"-cdt"))
-			misc|=CDT_;
+			misc|=CREDITS;
 		else if(!stricmp(argv[i],"-tot"))
 			misc|=TOT;
 		else if(!stricmp(argv[i],"-ext"))
@@ -260,18 +296,17 @@ int main(int argc, char **argv)
 		else if(!stricmp(argv[i],"--"))
 			misc|=MINUS;
 		else if(!stricmp(argv[i],"-*"))
-			misc|=(HDR|PAD|CDT_|PLUS|MINUS);
+			misc|=(HDR|PAD|CREDITS|PLUS|MINUS);
 
 		else if(i!=1) {
 			if(argv[i][0]=='*' || strcmp(argv[i],"-")==0) {
 				misc|=AUTO;
 				continue; 
 			}
-			if((j=nopen(argv[i],omode))==-1) {
-				printf("\nError opening/creating %s for output.\n",argv[i]);
+			if((out=fopen(argv[i], omode)) == NULL) {
+				perror(argv[i]);
 				exit(1); 
 			}
-			out=fdopen(j,"wb"); 
 		} 
 	}
 
@@ -293,119 +328,79 @@ int main(int argc, char **argv)
 		if(misc&AUTO && scfg.dir[i]->seqdev) 	/* CD-ROM */
 			continue;
 		printf("\n%-*s %s",LEN_GSNAME,scfg.lib[scfg.dir[i]->lib]->sname,scfg.dir[i]->lname);
-		sprintf(str,"%s%s.ixb",scfg.dir[i]->data_dir,scfg.dir[i]->code);
-		if((file=nopen(str,O_RDONLY))==-1)
+
+		smb_t smb;
+		int result = smb_open_dir(&scfg, &smb, i);
+		if(result != SMB_SUCCESS) {
+			fprintf(stderr, "!ERROR %d (%s) opening file base: %s\n", result, smb.last_error, scfg.dir[i]->code);
 			continue;
-		l=filelength(file);
+		}
+		time_t t = 0;
+		if(max_age)
+			t = time(NULL) - (max_age * 24 * 60 * 60);
+		ulong file_count;
+		file_t* file_list = loadfiles(&smb
+			,/* filespec: */pattern, /* time: */t, /* extdesc: */TRUE, scfg.dir[i]->sort, &file_count);
+
 		if(misc&AUTO) {
 			sprintf(str,"%sFILES.BBS",scfg.dir[i]->path);
-			if((j=nopen(str,omode))==-1) {
-				printf("\nError opening/creating %s for output.\n",str);
+			if((out=fopen(str, omode)) == NULL) {
+				perror(str);
 				exit(1); 
 			}
-			out=fdopen(j,"wb"); 
 		}
 		if(misc&HDR) {
 			sprintf(fname,"%-*s      %-*s       Files: %4lu"
 				,LEN_GSNAME,scfg.lib[scfg.dir[i]->lib]->sname
-				,LEN_SLNAME,scfg.dir[i]->lname,l/F_IXBSIZE);
-			fprintf(out,"%s\r\n",fname);
+				,LEN_SLNAME,scfg.dir[i]->lname, (ulong)smb.status.total_files);
+			fprintf(out,"%s\n",fname);
 			memset(fname,'-',strlen(fname));
-			fprintf(out,"%s\r\n",fname); 
+			fprintf(out,"%s\n",fname); 
 		}
-		if(!l) {
-			close(file);
+		if(!smb.status.total_files) {
 			if(misc&AUTO) fclose(out);
 			continue; 
 		}
-		if((ixbbuf=(uchar *)malloc(l))==NULL) {
-			close(file);
-			if(misc&AUTO) fclose(out);
-			printf("\7ERR_ALLOC %s %lu\n",str,l);
-			continue; 
+		int longest_filename = 12;
+		for(m = 0; m < file_count; m++) {
+			int fnamelen = strlen(file_list[m].name);
+			if(fnamelen > longest_filename)
+				longest_filename = fnamelen;
 		}
-		if(read(file,ixbbuf,l)!=(int)l) {
-			close(file);
-			if(misc&AUTO) fclose(out);
-			printf("\7ERR_READ %s %lu\n",str,l);
-			free((char *)ixbbuf);
-			continue; 
-		}
-		close(file);
-		sprintf(str,"%s%s.dat",scfg.dir[i]->data_dir,scfg.dir[i]->code);
-		if((file=nopen(str,O_RDONLY))==-1) {
-			printf("\7ERR_OPEN %s %u\n",str,O_RDONLY);
-			free((char *)ixbbuf);
-			if(misc&AUTO) fclose(out);
-			continue; 
-		}
-		datbuflen=filelength(file);
-		if((datbuf=malloc(datbuflen))==NULL) {
-			close(file);
-			printf("\7ERR_ALLOC %s %lu\n",str,datbuflen);
-			free((char *)ixbbuf);
-			if(misc&AUTO) fclose(out);
-			continue; 
-		}
-		if(read(file,datbuf,datbuflen)!=(int)datbuflen) {
-			close(file);
-			printf("\7ERR_READ %s %lu\n",str,datbuflen);
-			free((char *)datbuf);
-			free((char *)ixbbuf);
-			if(misc&AUTO) fclose(out);
-			continue; 
-		}
-		close(file);
-		m=0L;
-		while(m<l && !ferror(out)) {
-			for(j=0;j<12 && m<l;j++)
-				if(j==8)
-					str[j]=ixbbuf[m]>' ' ? '.' : ' ';
-				else
-					str[j]=ixbbuf[m++]; /* Turns FILENAMEEXT into FILENAME.EXT */
-			str[j]=0;
-			unpadfname(str,fname);
-			n=ixbbuf[m]|((long)ixbbuf[m+1]<<8)|((long)ixbbuf[m+2]<<16);
-			uld=(ixbbuf[m+3]|((long)ixbbuf[m+4]<<8)|((long)ixbbuf[m+5]<<16)
-				|((long)ixbbuf[m+6]<<24));
-			dld=(ixbbuf[m+7]|((long)ixbbuf[m+8]<<8)|((long)ixbbuf[m+9]<<16)
-				|((long)ixbbuf[m+10]<<24));
-			m+=11;
-
-			if(n>=datbuflen 							/* index out of bounds */
-				|| datbuf[n+F_DESC+LEN_FDESC]!=CR) {	/* corrupted data */
-				fprintf(stderr,"\n\7%s%s is corrupted!\n"
-					,scfg.dir[i]->data_dir,scfg.dir[i]->code);
-				exit(-1); 
-			}
-			
-			if(max_age && ((now - uld) / (24*60*60) > max_age))
-				continue;
-
-			fprintf(out,"%-12.12s",misc&PAD ? str : fname);
+		for(m = 0; m < file_count && !ferror(out); m++) {
+			file_t file = file_list[m];
+
+			if(misc&PAD) {
+				char* ext = getfext(file.name);
+				if(ext == NULL)
+					ext="";
+				fprintf(out,"%-*.*s%s"
+					, (int)(longest_filename - strlen(ext))
+					, (int)(strlen(file.name) - strlen(ext))
+					, file.name, ext);
+			} else
+				fprintf(out,"%-*s", longest_filename, file.name);
 
 			total_files++;
 			dir_files++;
 
-			if(misc&PLUS && datbuf[n+F_MISC]!=ETX
-				&& (datbuf[n+F_MISC]-' ')&FM_EXTDESC)
+			if(misc&PLUS && file.extdesc != NULL && file.extdesc[0])
 				fputc('+',out);
 			else
 				fputc(' ',out);
 
-			desc_off=12;
-			if(misc&(CDT_|TOT)) {
-				getrec((char *)&datbuf[n],F_CDT,LEN_FCDT,str);
-				cdt=atol(str);
+			desc_off = longest_filename;
+			if(misc&(CREDITS|TOT)) {
+				cdt=file.cost;
 				total_cdt+=cdt;
-				if(misc&CDT_) {
-					fprintf(out,"%7lu",cdt);
-					desc_off+=7; 
+				if(misc&CREDITS) {
+//					fprintf(out,"%7lu",cdt);
+					desc_off += fprintf(out, "%7s", byteStr(cdt));
 				} 
 			}
 
 			if(misc&MINUS) {
-				SAFEPRINTF2(str,"%s%s",scfg.dir[i]->path,fname);
+				sprintf(str,"%s%s",scfg.dir[i]->path,file.name);
 				if(!fexistcase(str))
 					fputc('-',out);
 				else
@@ -416,88 +411,56 @@ int main(int argc, char **argv)
 			desc_off++;
 
 			if(misc&DFD) {
-				SAFEPRINTF2(str,"%s%s",scfg.dir[i]->path,fname);
-				fprintf(out,"%s ",unixtodstr(&scfg,(time32_t)fdate(str),str));
-				desc_off+=9; 
+				// TODO: Fix to support alt-file-paths:
+				sprintf(str,"%s%s",scfg.dir[i]->path,file.name);
+				desc_off += fprintf(out,"%s ",unixtodstr(&scfg,(time32_t)fdate(str),str));
 			}
 
 			if(misc&ULD) {
-				fprintf(out,"%s ",unixtodstr(&scfg,uld,str));
-				desc_off+=9; 
+				desc_off += fprintf(out,"%s ",unixtodstr(&scfg,file.hdr.when_imported.time,str));
 			}
 
 			if(misc&ULN) {
-				getrec((char *)&datbuf[n],F_ULER,25,str);
-				fprintf(out,"%-25s ",str);
-				desc_off+=26; 
+				desc_off += fprintf(out,"%-25s ",file.from);
 			}
 
 			if(misc&DLD) {
-				fprintf(out,"%s ",unixtodstr(&scfg,dld,str));
-				desc_off+=9; 
+				desc_off += fprintf(out,"%s ",unixtodstr(&scfg,file.hdr.last_downloaded,str));
 			}
 
 			if(misc&DLS) {
-				getrec((char *)&datbuf[n],F_TIMESDLED,5,str);
-				j=atoi(str);
-				fprintf(out,"%5u ",j);
-				desc_off+=6; 
+				desc_off += fprintf(out,"%5u ",file.hdr.times_downloaded);
 			}
 
-			if(datbuf[n+F_MISC]!=ETX && (datbuf[n+F_MISC]-' ')&FM_EXTDESC)
+			if(file.extdesc != NULL && file.extdesc[0])
 				ext=1;	/* extended description exists */
 			else
 				ext=0;	/* it doesn't */
 
 			if(!(misc&NOD) && !(misc&NOE && ext)) {
-				getrec((char *)&datbuf[n],F_DESC,LEN_FDESC,str);
-				fprintf(out,"%s",str); 
+				fprintf(out,"%s",file.desc); 
 			}
 
 			if(misc&EXT && ext) {							/* Print ext desc */
 
-				sprintf(str,"%s%s.exb",scfg.dir[i]->data_dir,scfg.dir[i]->code);
-				if(!fexist(str))
-					continue;
-				if((j=nopen(str,O_RDONLY))==-1) {
-					printf("\7ERR_OPEN %s %u\n",str,O_RDONLY);
-					continue; 
-				}
-				if((in=fdopen(j,"rb"))==NULL) {
-					close(j);
-					continue; 
-				}
-				fseek(in,(n/F_LEN)*512L,SEEK_SET);
 				lines=0;
 				if(!(misc&NOE)) {
-					fprintf(out,"\r\n");
-					lines++; 
-				}
-				while(!feof(in) && !ferror(in)
-					&& ftell(in)<(long)((n/F_LEN)+1)*512L) {
-					if(!fgets(str,128,in) || !str[0])
-						break;
-					stripctrlz(str);
-					if(lines) {
-						if(misc&JST)
-							fprintf(out,"%*s",desc_off,"");
-						fputc(' ',out);				/* indent one character */ }
-					fprintf(out,"%s",str);
+					truncsp((char*)file.extdesc);
+					fprint_extdesc(out, file.extdesc, (misc&JST) ? desc_off : 0);
 					lines++; 
 				}
-				fclose(in); 
 			}
-			fprintf(out,"\r\n"); 
+			fprintf(out,"\n"); 
 		}
-		free((char *)datbuf);
-		free((char *)ixbbuf);
+		smb_close(&smb);
 		if(dir_files)
-			fprintf(out,"\r\n"); /* blank line at end of dir */
+			fprintf(out,"\n"); /* blank line at end of dir */
 		if(misc&AUTO) fclose(out); 
+		freefiles(file_list, file_count);
 	}
 
 	if(misc&TOT && !(misc&AUTO))
-		fprintf(out,"TOTALS\n------\n%lu credits/bytes in %lu files.\r\n"
+		fprintf(out,"TOTALS\n------\n%lu credits/bytes in %lu files.\n"
 			,total_cdt,total_files);
 	printf("\nDone.\n");
 	return(0);
diff --git a/src/sbbs3/filelist.vcxproj b/src/sbbs3/filelist.vcxproj
index 4b662e6cc44e6b4ee2e3670b0a9eea9e80a10d10..84aa43f941bf2ff167ca459b88831a0855ccbbd4 100644
--- a/src/sbbs3/filelist.vcxproj
+++ b/src/sbbs3/filelist.vcxproj
@@ -38,6 +38,7 @@
     <Import Project="..\build\target_ia32.props" />
     <Import Project="..\hash\hash.props" />
     <Import Project="..\encode\encode.props" />
+    <Import Project="..\..\3rdp\win32.release\libarchive\libarchive.props" />
   </ImportGroup>
   <ImportGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="PropertySheets">
     <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
@@ -48,6 +49,7 @@
     <Import Project="..\build\target_ia32.props" />
     <Import Project="..\hash\hash.props" />
     <Import Project="..\encode\encode.props" />
+    <Import Project="..\..\3rdp\win32.release\libarchive\libarchive.props" />
   </ImportGroup>
   <PropertyGroup Label="UserMacros" />
   <PropertyGroup>
@@ -168,8 +170,12 @@
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
     </ClCompile>
+    <ClCompile Include="userdat.c" />
   </ItemGroup>
   <ItemGroup>
+    <ProjectReference Include="..\smblib\smblib.vcxproj">
+      <Project>{d674842b-2f41-42cb-9426-b3c4b0682574}</Project>
+    </ProjectReference>
     <ProjectReference Include="..\xpdev\xpdev.vcxproj">
       <Project>{7428a1e8-56b7-4868-9c0e-29d031689feb}</Project>
       <ReferenceOutputAssembly>false</ReferenceOutputAssembly>
diff --git a/src/sbbs3/fixsmb.c b/src/sbbs3/fixsmb.c
index 343d57ff49c67b8c300b8ed5f76bf77294230adc..3a2e6cfceb72568529a25c9904e7b7e346bdfba6 100644
--- a/src/sbbs3/fixsmb.c
+++ b/src/sbbs3/fixsmb.c
@@ -1,8 +1,5 @@
 /* Synchronet message base (SMB) index re-generator */
 
-/* $Id: fixsmb.c,v 1.46 2018/04/30 06:05:12 rswindell Exp $ */
-// vi: tabstop=4
-
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
  * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
@@ -16,21 +13,9 @@
  * See the GNU General Public License for more details: gpl.txt or			*
  * http://www.fsf.org/copyleft/gpl.html										*
  *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
- *																			*
  * For Synchronet coding style and modification guidelines, see				*
  * http://www.synchro.net/source.html										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
@@ -44,6 +29,8 @@
 #include "genwrap.h"	/* PLATFORM_DESC */
 #include "str_list.h"	/* strList API */
 #include "crc16.h"
+#include "git_branch.h"
+#include "git_hash.h"
 
 smb_t	smb;
 BOOL	renumber=FALSE;
@@ -60,22 +47,23 @@ int compare_index(const idxrec_t* idx1, const idxrec_t* idx2)
 void sort_index(smb_t* smb)
 {
 	ulong		l;
-	idxrec_t*	idx;
+	uint8_t*	idxbuf;
+	size_t		idxreclen = smb_idxreclen(smb);
 
 	printf("Sorting index... ");
-	if((idx=malloc(sizeof(idxrec_t)*smb->status.total_msgs))==NULL) {
+	if((idxbuf = malloc(idxreclen * smb->status.total_msgs))==NULL) {
 		perror("malloc");
 		return;
 	}
 
 	rewind(smb->sid_fp);
 	for(l=0;l<smb->status.total_msgs;l++)
-		if(fread(&idx[l],sizeof(idxrec_t),1,smb->sid_fp)<1) {
+		if(smb_fread(smb, idxbuf + (l * idxreclen), idxreclen, smb->sid_fp) != idxreclen) {
 			perror("reading index");
 			break;
 		}
 
-	qsort(idx,l,sizeof(idxrec_t)
+	qsort(idxbuf, l, idxreclen
 		,(int(*)(const void*, const void*))compare_index);
 
 	rewind(smb->sid_fp);
@@ -83,13 +71,13 @@ void sort_index(smb_t* smb)
 
 	printf("\nRe-writing index... \n");
 	smb->status.total_msgs=l;
-	for(l=0;l<smb->status.total_msgs;l++)
-		if(fwrite(&idx[l],sizeof(idxrec_t),1,smb->sid_fp)<1) {
+	for(l=0;l<smb->status.total_msgs;l++) {
+		if(smb_fwrite(smb, idxbuf + (l * idxreclen), idxreclen, smb->sid_fp) != idxreclen) {
 			perror("writing index");
 			break;
 		}
-
-	free(idx);
+	}
+	free(idxbuf);
 	printf("\n");
 }
 
@@ -110,7 +98,8 @@ int fixsmb(char* sub)
 	char*		text;
 	char		c;
 	int 		i,w;
-	ulong		l,length,size,n;
+	ulong		l,size,n;
+	off_t		length;
 	smbmsg_t	msg;
 	uint32_t*	numbers = NULL;
 	long		total = 0;
@@ -205,11 +194,12 @@ int fixsmb(char* sub)
 	} else
 		length=filelength(fileno(smb.shd_fp));
 
-	n=0;	/* messsage offset */
+	n=0;	/* message offset */
 	for(l=smb.status.header_offset;l<length;l+=size) {
 		size=SHD_BLOCK_LEN;
 		printf("\r%2lu%%  ",(long)(100.0/((float)length/l)));
 		fflush(stdout);
+		ZERO_VAR(msg);
 		msg.idx.offset=l;
 		if((i=smb_lockmsghdr(&smb,&msg))!=0) {
 			printf("\n(%06lX) smb_lockmsghdr returned %d:\n%s\n",l,i,smb.last_error);
@@ -222,7 +212,8 @@ int fixsmb(char* sub)
 			continue;
 		}
 		size=smb_hdrblocks(smb_getmsghdrlen(&msg))*SHD_BLOCK_LEN;
-		printf("#%-5"PRIu32" (%06lX) %-25.25s ",msg.hdr.number,l,msg.from);
+		printf("#%-5"PRIu32" (%06lX) %-25.25s ",msg.hdr.number,l
+			,msg.hdr.type == SMB_MSG_TYPE_FILE ? msg.subj : msg.from);
 
 		dupe_msgnum = FALSE;
 		for(i=0; i<total && !dupe_msgnum; i++)
@@ -267,7 +258,7 @@ int fixsmb(char* sub)
 		else if(msg.hdr.number==0)
 			printf("Not indexing invalid message number (0)!\n");
 		else {
-			msg.offset=n;
+			msg.idx_offset=n;
 			if(renumber)
 				msg.hdr.number=n+1;
 			if(msg.hdr.number > highest)
@@ -327,15 +318,12 @@ int fixsmb(char* sub)
 
 int main(int argc, char **argv)
 {
-	char		revision[16];
 	int 		i;
 	str_list_t	list;
 	int			retval = EXIT_SUCCESS;
 
-	sscanf("$Revision: 1.46 $", "%*s %s", revision);
-
-	printf("\nFIXSMB v2.10-%s (rev %s) SMBLIB %s - Rebuild Synchronet Message Base\n\n"
-		,PLATFORM_DESC,revision,smb_lib_ver());
+	printf("\nFIXSMB v3.19-%s %s/%s SMBLIB %s - Rebuild Synchronet Message Base\n\n"
+		,PLATFORM_DESC, GIT_BRANCH, GIT_HASH, smb_lib_ver());
 
 	list=strListInit();
 
diff --git a/src/sbbs3/ftpsrvr.c b/src/sbbs3/ftpsrvr.c
index 5ae915ec5489c5d93b8a4364bd72d87a15fcf92b..e009f2d9366442815681da8c1568e9ce915fcc3d 100644
--- a/src/sbbs3/ftpsrvr.c
+++ b/src/sbbs3/ftpsrvr.c
@@ -35,13 +35,15 @@
 #include "sbbs.h"
 #include "text.h"			/* TOTAL_TEXT */
 #include "ftpsrvr.h"
+#include "filedat.h"
 #include "telnet.h"
 #include "multisock.h"
 #include "ssl.h"
 #include "cryptlib.h"
 #include "xpprintf.h"		// vasprintf
 #include "md5.h"
-#include "ver.h"
+#include "git_branch.h"
+#include "git_hash.h"
 
 /* Constants */
 
@@ -597,7 +599,6 @@ typedef struct {
 static void send_thread(void* arg)
 {
 	char		buf[8192];
-	char		fname[MAX_PATH+1];
 	char		str[256];
 	char		tmp[128];
 	char		username[128];
@@ -633,11 +634,15 @@ static void send_thread(void* arg)
 	length=flength(xfer.filename);
 
 	if(length < 1) {
-		lprintf(LOG_WARNING, "%04d <%s> !DATA cannot send file (%s) with size of %"PRIdOFF" bytes"
-			,xfer.ctrl_sock, xfer.user->alias, xfer.filename, length);
-		sockprintf(xfer.ctrl_sock,xfer.ctrl_sess,"450 Invalid file size: %"PRIdOFF, length);
-		if(xfer.tmpfile && !(startup->options&FTP_OPT_KEEP_TEMP_FILES))
-			(void)ftp_remove(xfer.ctrl_sock, __LINE__, xfer.filename, xfer.user->alias);
+		if(xfer.tmpfile) {
+			if(!(startup->options&FTP_OPT_KEEP_TEMP_FILES))
+				ftp_remove(xfer.ctrl_sock, __LINE__, xfer.filename, xfer.user->alias);
+			sockprintf(xfer.ctrl_sock,xfer.ctrl_sess,"450 No files");
+		} else {
+			lprintf(LOG_WARNING, "%04d <%s> !DATA cannot send file (%s) with size of %"PRIdOFF" bytes"
+				,xfer.ctrl_sock, xfer.user->alias, xfer.filename, length);
+			sockprintf(xfer.ctrl_sock,xfer.ctrl_sess,"450 Invalid file size: %"PRIdOFF, length);
+		}
 		ftp_close_socket(xfer.data_sock,xfer.data_sess,__LINE__);
 		*xfer.inprogress=FALSE;
 		thread_down();
@@ -668,7 +673,7 @@ static void send_thread(void* arg)
 		lprintf(LOG_DEBUG,"%04d <%s> DATA socket %d sending %s from offset %"PRIdOFF
 			,xfer.ctrl_sock, xfer.user->alias, *xfer.data_sock,xfer.filename,xfer.filepos);
 
-	(void)fseek(fp,xfer.filepos,SEEK_SET);
+	fseeko(fp,xfer.filepos,SEEK_SET);
 	last_report=start=time(NULL);
 	while((xfer.filepos+total)<length) {
 
@@ -705,7 +710,7 @@ static void send_thread(void* arg)
 		if (!socket_writable(*xfer.data_sock, 1000))
 			continue;
 
-		(void)fseek(fp,xfer.filepos+total,SEEK_SET);
+		fseeko(fp,xfer.filepos+total,SEEK_SET);
 		rd=fread(buf,sizeof(char),sizeof(buf),fp);
 		if(rd<1) /* EOF or READ error */
 			break;
@@ -781,7 +786,7 @@ static void send_thread(void* arg)
 	
 	if(!error) {
 		dur=(long)(time(NULL)-start);
-		cps=dur ? total/dur : total*2;
+		cps=(ulong)(dur ? total/dur : total*2);
 		lprintf(LOG_INFO,"%04d <%s> DATA Transfer successful: %"PRIdOFF" bytes sent in %lu seconds (%lu cps)"
 			,xfer.ctrl_sock
 			,xfer.user->alias
@@ -790,34 +795,29 @@ static void send_thread(void* arg)
 
 		if(xfer.dir>=0) {
 			memset(&f,0,sizeof(f));
-#ifdef _WIN32
-			GetShortPathName(xfer.filename,fname,sizeof(fname));
-#else
-			SAFECOPY(fname,xfer.filename);
-#endif
-			padfname(getfname(fname),f.name);
-			f.dir=xfer.dir;
-			f.size=total;
-			if(getfileixb(&scfg,&f)==TRUE && getfiledat(&scfg,&f)==TRUE) {
-				f.timesdled++;
-				putfiledat(&scfg,&f);
-				f.datedled=time32(NULL);
-				putfileixb(&scfg,&f);
+			if(loadfile(&scfg, xfer.dir, xfer.filename, &f, file_detail_normal) == TRUE) {
+				f.hdr.times_downloaded++;
+				f.hdr.last_downloaded = time32(NULL);
+				updatefile(&scfg, &f);
 
 				lprintf(LOG_INFO,"%04d <%s> DATA downloaded: %s (%u times total)"
 					,xfer.ctrl_sock
 					,xfer.user->alias
 					,xfer.filename
-					,f.timesdled);
+					,f.hdr.times_downloaded);
 				/**************************/
 				/* Update Uploader's Info */
 				/**************************/
-				uploader.number=matchuser(&scfg,f.uler,TRUE /*sysop_alias*/);
+				uploader.number = 0;
+				if(f.from_ext != NULL)
+					uploader.number = atoi(f.from_ext);
+				if(uploader.number == 0)
+					uploader.number=matchuser(&scfg, f.from, TRUE /*sysop_alias*/);
 				if(uploader.number
 					&& uploader.number!=xfer.user->number 
 					&& getuserdat(&scfg,&uploader)==0
-					&& uploader.firston<f.dateuled) {
-					l=f.cdt;
+					&& uploader.firston < (time_t)f.hdr.when_imported.time) {
+					l=f.cost;
 					if(!(scfg.dir[f.dir]->misc&DIR_CDTDL))	/* Don't give credits on d/l */
 						l=0;
 					if(scfg.dir[f.dir]->misc&DIR_CDTMIN && cps) { /* Give min instead of cdt */
@@ -847,7 +847,7 @@ static void send_thread(void* arg)
 				}
 			}
 			if(!xfer.tmpfile && !xfer.delfile && !(scfg.dir[f.dir]->misc&DIR_NOSTAT))
-				inc_sys_download_stats(&scfg, 1, total);
+				inc_sys_download_stats(&scfg, 1, (ulong)total);
 		}	
 
 		if(xfer.credits) {
@@ -876,17 +876,11 @@ static void send_thread(void* arg)
 
 static void receive_thread(void* arg)
 {
-	char*		p;
 	char		str[128];
 	char		buf[8192];
-	char		ext[F_EXBSIZE+1];
-	char		desc[F_EXBSIZE+1];
-	char		cmd[MAX_PATH*2];
+	char		extdesc[LEN_EXTDESC + 1] = "";
 	char		tmp[MAX_PATH+1];
-	char		fname[MAX_PATH+1];
-	int			i;
 	int			rd;
-	int			file;
 	off_t		total=0;
 	off_t		last_total=0;
 	ulong		dur;
@@ -894,7 +888,7 @@ static void receive_thread(void* arg)
 	BOOL		error=FALSE;
 	BOOL		filedat;
 	FILE*		fp;
-	file_t		f;
+	file_t	f;
 	xfer_t		xfer;
 	time_t		now;
 	time_t		start;
@@ -929,7 +923,7 @@ static void receive_thread(void* arg)
 		lprintf(LOG_DEBUG,"%04d <%s> DATA socket %d receiving %s from offset %"PRIdOFF
 			,xfer.ctrl_sock,xfer.user->alias, *xfer.data_sock,xfer.filename,xfer.filepos);
 
-	(void)fseek(fp,xfer.filepos,SEEK_SET);
+	fseeko(fp,xfer.filepos,SEEK_SET);
 	last_report=start=time(NULL);
 	while(1) {
 
@@ -1051,7 +1045,7 @@ static void receive_thread(void* arg)
 			(void)ftp_remove(xfer.ctrl_sock, __LINE__, xfer.filename, xfer.user->alias);
 	} else {
 		dur=(long)(time(NULL)-start);
-		cps=dur ? total/dur : total*2;
+		cps=(ulong)(dur ? total/dur : total*2);
 		lprintf(LOG_INFO,"%04d <%s> DATA Transfer successful: %"PRIdOFF" bytes received in %lu seconds (%lu cps)"
 			,xfer.ctrl_sock
 			,xfer.user->alias
@@ -1059,91 +1053,51 @@ static void receive_thread(void* arg)
 
 		if(xfer.dir>=0) {
 			memset(&f,0,sizeof(f));
-#ifdef _WIN32
-			GetShortPathName(xfer.filename,fname,sizeof(fname));
-#else
-			SAFECOPY(fname,xfer.filename);
-#endif
-			padfname(getfname(fname),f.name);
-			f.dir=xfer.dir;
-			filedat=getfileixb(&scfg,&f);
+			smb_hfield_str(&f, SMB_FILENAME, getfname(xfer.filename));
+			smb_hfield_str(&f, SENDER, xfer.user->alias);
+
+			filedat=findfile(&scfg, xfer.dir, f.name, NULL);
 			if(scfg.dir[f.dir]->misc&DIR_AONLY)  /* Forced anonymous */
-				f.misc|=FM_ANON;
-			f.cdt=flength(xfer.filename);
-			f.dateuled=time32(NULL);
+				f.hdr.attr |= MSG_ANONYMOUS;
+			off_t cdt = flength(xfer.filename);
+			smb_hfield_bin(&f, SMB_COST, cdt);
 
+			char fdesc[LEN_FDESC + 1] = "";
 			/* Description specified with DESC command? */
-			if(xfer.desc!=NULL && *xfer.desc!=0)	
-				SAFECOPY(f.desc,xfer.desc);
+			if(xfer.desc != NULL)	
+				SAFECOPY(fdesc, xfer.desc);
 
 			/* Necessary for DIR and LIB ARS keyword support in subsequent chk_ar()'s */
 			SAFECOPY(xfer.user->curdir, scfg.dir[f.dir]->code);
 
 			/* FILE_ID.DIZ support */
-			p=strrchr(f.name,'.');
-			if(p!=NULL && scfg.dir[f.dir]->misc&DIR_DIZ) {
-				for(i=0;i<scfg.total_fextrs;i++)
-					if(!stricmp(scfg.fextr[i]->ext,p+1) 
-						&& chk_ar(&scfg,scfg.fextr[i]->ar,xfer.user,xfer.client))
-						break;
-				if(i<scfg.total_fextrs) {
-					sprintf(tmp,"%sFILE_ID.DIZ",scfg.temp_dir);
-					if(fexistcase(tmp))
-						(void)ftp_remove(xfer.ctrl_sock, __LINE__, tmp, xfer.user->alias);
-					cmdstr(&scfg,xfer.user,scfg.fextr[i]->cmd,fname,"FILE_ID.DIZ",cmd);
-					lprintf(LOG_DEBUG,"%04d <%s> DATA Extracting DIZ: %s",xfer.ctrl_sock, xfer.user->alias,cmd);
-					system(cmd);
-					if(!fexistcase(tmp)) {
-						sprintf(tmp,"%sDESC.SDI",scfg.temp_dir);
-						if(fexistcase(tmp))
-							(void)ftp_remove(xfer.ctrl_sock, __LINE__, tmp, xfer.user->alias);
-						cmdstr(&scfg,xfer.user,scfg.fextr[i]->cmd,fname,"DESC.SDI",cmd);
-						lprintf(LOG_DEBUG,"%04d <%s> DATA Extracting DIZ: %s",xfer.ctrl_sock, xfer.user->alias,cmd);
-						system(cmd); 
-						fexistcase(tmp);	/* fixes filename case */
+			if(scfg.dir[f.dir]->misc&DIR_DIZ) {
+				lprintf(LOG_DEBUG,"%04d <%s> DATA Extracting DIZ from: %s",xfer.ctrl_sock, xfer.user->alias,xfer.filename);
+				if(extract_diz(&scfg, &f, /* diz_fnames */NULL, tmp, sizeof(tmp))) {
+					lprintf(LOG_DEBUG,"%04d <%s> DATA Parsing DIZ: %s",xfer.ctrl_sock, xfer.user->alias,tmp);
+					str_list_t lines = read_diz(tmp, /* max_line_len: */80);
+					format_diz(lines, extdesc, sizeof(extdesc), /* allow_ansi: */false);
+					strListFree(&lines);
+					if(!fdesc[0]) {						/* use for normal description */
+						prep_file_desc(extdesc, fdesc);	/* strip control chars and dupe chars */
 					}
-					if((file=nopen(tmp,O_RDONLY))!=-1) {
-						lprintf(LOG_DEBUG,"%04d <%s> DATA Parsing DIZ: %s",xfer.ctrl_sock, xfer.user->alias,tmp);
-						memset(ext,0,sizeof(ext));
-						read(file,ext,sizeof(ext)-1);
-						for(i=sizeof(ext)-1;i;i--)	/* trim trailing spaces */
-							if(ext[i-1]>' ')
-								break;
-						ext[i]=0;
-						if(!f.desc[0]) {			/* use for normal description */
-							SAFECOPY(desc,ext);
-							strip_exascii(desc, desc);	/* strip extended ASCII chars */
-							prep_file_desc(desc, desc);	/* strip control chars and dupe chars */
-							for(i=0;desc[i];i++)	/* find appropriate first char */
-								if(IS_ALPHANUMERIC(desc[i]))
-									break;
-							SAFECOPY(f.desc,desc+i); 
-						}
-						close(file);
-						(void)ftp_remove(xfer.ctrl_sock, __LINE__, tmp, xfer.user->alias);
-						f.misc|=FM_EXTDESC; 
-					} else
-						lprintf(LOG_DEBUG,"%04d <%s> DATA DIZ Does not exist: %s",xfer.ctrl_sock, xfer.user->alias,tmp);
-				} 
+					ftp_remove(xfer.ctrl_sock, __LINE__, tmp, xfer.user->alias);
+				} else
+					lprintf(LOG_DEBUG,"%04d <%s> DATA DIZ does not exist in: %s",xfer.ctrl_sock, xfer.user->alias ,xfer.filename);
 			} /* FILE_ID.DIZ support */
 
-			if(f.desc[0]==0) 	/* no description given, use (long) filename */
-				SAFECOPY(f.desc,getfname(xfer.filename));
-
-			SAFECOPY(f.uler,xfer.user->alias);	/* exception here, Aug-27-2002 */
+			smb_hfield_str(&f, SMB_FILEDESC, fdesc);
 			if(filedat) {
-				if(!putfiledat(&scfg,&f))
+				if(!updatefile(&scfg, &f))
 					lprintf(LOG_ERR,"%04d <%s> !DATA ERROR updating file (%s) in database"
-						,xfer.ctrl_sock, xfer.user->alias,f.name);
+						,xfer.ctrl_sock, xfer.user->alias, f.name);
 				/* need to update the index here */
 			} else {
-				if(!addfiledat(&scfg,&f))
+				if(!addfile(&scfg, xfer.dir, &f, extdesc))
 					lprintf(LOG_ERR,"%04d <%s> !DATA ERROR adding file (%s) to database"
-						,xfer.ctrl_sock, xfer.user->alias,f.name);
+						,xfer.ctrl_sock, xfer.user->alias, f.name);
 			}
-
-			if(f.misc&FM_EXTDESC)
-				putextdesc(&scfg,f.dir,f.datoffset,ext);
+			smb_freefilemem(&f);
 
 			if(scfg.dir[f.dir]->upload_sem[0])
 				ftouch(scfg.dir[f.dir]->upload_sem);
@@ -1157,10 +1111,10 @@ static void receive_thread(void* arg)
 						,((ulong)(total*(scfg.dir[f.dir]->up_pct/100.0))/cps)/60);
 				else
 					xfer.user->cdt=adjustuserrec(&scfg,xfer.user->number,U_CDT,10
-						,(ulong)(f.cdt*(scfg.dir[f.dir]->up_pct/100.0))); 
+						,(ulong)(cdt*(scfg.dir[f.dir]->up_pct/100.0))); 
 			}
 			if(!(scfg.dir[f.dir]->misc&DIR_NOSTAT))
-				inc_sys_upload_stats(&scfg, 1, total);
+				inc_sys_upload_stats(&scfg, 1, (ulong)total);
 		}
 		/* Send ACK */
 		sockprintf(xfer.ctrl_sock,sess,"226 Upload complete (%lu cps).",cps);
@@ -1463,6 +1417,10 @@ char* dotname(char* in, char* out)
 	char	ch;
 	int		i;
 
+	if(in == NULL) {
+		strcpy(out, "(null)");
+		return out;
+	}
 	if(strchr(in,'.')==NULL)
 		ch='.';
 	else
@@ -1847,7 +1805,7 @@ static char *get_unique(const char *path, char *uniq)
 		return NULL;
 
 	MD5_calc(digest, path, strlen(path));
-	MD5_hex((BYTE*)uniq, digest);
+	MD5_hex(uniq, digest);
 	return uniq;
 }
 
@@ -1942,7 +1900,7 @@ static BOOL write_local_mlsx(FILE *fp, SOCKET sock, CRYPT_SESSION sess, unsigned
 	*p=0;
 	if (is_file)
 		full_path = FALSE;
-	return send_mlsx_entry(fp, sock, sess, feats, type, permstr, (uint64_t)st.st_size, st.st_mtime, NULL, NULL, 0, full_path ? path : getfname(path));
+	return send_mlsx_entry(fp, sock, sess, feats, type, permstr, (uint64_t)st.st_size, st.st_mtime, NULL, NULL, st.st_ctime, full_path ? path : getfname(path));
 }
 
 /*
@@ -2037,11 +1995,8 @@ static BOOL can_append(lib_t *lib, dir_t *dir, user_t *user, client_t *client, f
 		if(!chk_ar(&scfg,dir->ul_ar,user,client))
 			return FALSE;
 	}
-	if(!getfileixb(&scfg,file) || !getfiledat(&scfg,file))
-		return FALSE;
-	if (stricmp(file->uler,user->alias))
+	if (file->from == NULL || stricmp(file->from, user->alias) != 0)
 		return FALSE;
-	// Check credits?
 	return TRUE;
 }
 
@@ -2057,8 +2012,6 @@ static BOOL can_delete(lib_t *lib, dir_t *dir, user_t *user, client_t *client, f
 		return FALSE;
 	if (!(user->exempt&FLAG('R')))
 		return FALSE;
-	if(!getfileixb(&scfg,file) && !(startup->options&FTP_OPT_DIR_FILES) && !(dir->misc&DIR_FILES))
-		return FALSE;
 	return TRUE;
 }
 
@@ -2072,8 +2025,6 @@ static BOOL can_download(lib_t *lib, dir_t *dir, user_t *user, client_t *client,
 		return FALSE;
 	if (!chk_ar(&scfg,dir->dl_ar,user,client))
 		return FALSE;
-	if(!getfileixb(&scfg,file) && !(startup->options&FTP_OPT_DIR_FILES) && !(dir->misc&DIR_FILES))
-		return FALSE;
 	// TODO: Verify credits
 	return TRUE;
 }
@@ -2103,10 +2054,10 @@ static void get_owner_name(file_t *file, char *namestr)
 	char *p;
 
 	if (file) {
-		if (file->misc & FM_ANON)
+		if (file->hdr.attr & MSG_ANONYMOUS)
 			strcpy(namestr, ANONYMOUS);
 		else
-			strcpy(namestr, file->uler);
+			strcpy(namestr, file->from);
 	}
 	else
 		strcpy(namestr, scfg.sys_id);
@@ -2223,8 +2174,6 @@ static void ctrl_thread(void* arg)
 	time_t		lastactive;
 	time_t		file_date;
 	off_t		file_size;
-	file_t		f;
-	glob_t		g;
 	node_t		node;
 	client_t	client;
 	struct tm	tm;
@@ -3231,6 +3180,7 @@ static void ctrl_thread(void* arg)
 					}
 					else {
 						time_t start = time(NULL);
+						glob_t g;
 						glob(path, GLOB_MARK, NULL, &g);
 						for(i=0;i<(int)g.gl_pathc;i++) {
 							char fpath[MAX_PATH + 1];
@@ -3289,6 +3239,7 @@ static void ctrl_thread(void* arg)
 					memset(&cur_tm,0,sizeof(cur_tm));
 			
 				time_t start = time(NULL);
+				glob_t g;
 				glob(path, GLOB_MARK, NULL, &g);
 				for(i=0;i<(int)g.gl_pathc;i++) {
 					char fpath[MAX_PATH + 1];
@@ -3299,14 +3250,12 @@ static void ctrl_thread(void* arg)
 						struct stat st;
 						if(stat(fpath, &st) != 0)
 							continue;
-						f.size = st.st_size;
-						t = st.st_mtime;
-						if(localtime_r(&t,&tm)==NULL)
+						if(localtime_r(&st.st_mtime,&tm)==NULL)
 							memset(&tm,0,sizeof(tm));
-						fprintf(fp,"%crw-r--r--   1 %-8s local %9"PRId32" %s %2d "
+						fprintf(fp,"%crw-r--r--   1 %-8s local %9"PRId64" %s %2d "
 							,*lastchar(g.gl_pathv[i]) == '/' ? 'd':'-'
 							,scfg.sys_id
-							,f.size
+							,(int64_t)st.st_size
 							,ftp_mon[tm.tm_mon],tm.tm_mday);
 						if(tm.tm_year==cur_tm.tm_year)
 							fprintf(fp,"%02d:%02d %s\r\n"
@@ -3774,51 +3723,45 @@ static void ctrl_thread(void* arg)
 						get_unique(aliaspath, uniq);
 						send_mlsx_entry(fp, sock, sess, mlsx_feats, "cdir", permstr, UINT64_MAX, 0, str, NULL, 0, aliaspath);
 					}
-
+					smb_t smb;
+					if((result = smb_open_dir(&scfg, &smb, dir)) != SMB_SUCCESS) {
+						lprintf(LOG_ERR, "ERROR %d (%s) opening %s", result, smb.last_error, smb.file);
+						continue;
+					}
 					time_t start = time(NULL);
-					SAFEPRINTF2(path,"%s%s",scfg.dir[dir]->path,"*");
-					glob(path, GLOB_MARK, NULL, &g);
-					for(i=0;i<(int)g.gl_pathc;i++) {
-						if(*lastchar(g.gl_pathv[i]) == '/')	/* is directory */
-							continue;
-#ifdef _WIN32
-						GetShortPathName(g.gl_pathv[i], str, sizeof(str));
-#else
-						SAFECOPY(str,g.gl_pathv[i]);
-#endif
-						padfname(getfname(str),f.name);
-						f.dateuled = 0;
-						f.dir=dir;
-						if((filedat=getfileixb(&scfg,&f))==FALSE
-							&& !(startup->options&FTP_OPT_DIR_FILES)
-							&& !(scfg.dir[dir]->misc&DIR_FILES))
-							continue;
-						if (cmd[3] != 'D' && strcmp(getfname(g.gl_pathv[i]), mls_fname) != 0)
+					size_t file_count = 0;
+					file_t* file_list = loadfiles(&smb
+						,/* filespec */NULL, /* time: */0, file_detail_normal, scfg.dir[dir]->sort, &file_count);
+					for(size_t i = 0; i < file_count; i++) {
+						file_t* f = &file_list[i];
+						if (cmd[3] != 'D' && strcmp(f->name, mls_fname) != 0)
 							continue;
 						if (cmd[3] == 'T')
 							sockprintf(sock,sess, "250- Listing %s", p);
-						get_fileperm(scfg.lib[lib], scfg.dir[dir], &user, &client, &f, permstr);
-						get_owner_name(&f, str);
-						SAFEPRINTF3(aliaspath, "/%s/%s/%s", scfg.lib[lib]->sname, scfg.dir[dir]->code_suffix, getfname(g.gl_pathv[i]));
+						get_fileperm(scfg.lib[lib], scfg.dir[dir], &user, &client, f, permstr);
+						get_owner_name(f, str);
+						SAFEPRINTF3(aliaspath, "/%s/%s/%s", scfg.lib[lib]->sname, scfg.dir[dir]->code_suffix, f->name);
 						get_unique(aliaspath, uniq);
-						f.size = f.cdt;
-						f.date = f.dateuled;
-						if(!filedat || (scfg.dir[dir]->misc&DIR_FCHK)) {
+						f->size = f->cost;
+						f->time = f->hdr.when_imported.time;
+						if(scfg.dir[dir]->misc&DIR_FCHK) {
 							struct stat st;
-							if(stat(g.gl_pathv[i], &st) != 0)
+							if(stat(getfilepath(&scfg, f, path), &st) != 0)
 								continue;
-							f.size = st.st_size;
-							f.date = (time32_t)st.st_mtime;
+							f->size = st.st_size;
+							f->time = st.st_mtime;
+							f->hdr.when_imported.time = (uint32_t)st.st_ctime;
 						}
-						send_mlsx_entry(fp, sock, sess, mlsx_feats, "file", permstr, f.size, f.date, str, uniq, f.dateuled, cmd[3] == 'T' ? mls_path : getfname(g.gl_pathv[i]));
+						send_mlsx_entry(fp, sock, sess, mlsx_feats, "file", permstr, f->size, f->time, str, uniq, f->hdr.when_imported.time, cmd[3] == 'T' ? mls_path : f->name);
 						l++;
 					}
 					if (cmd[3] == 'D') {
 						lprintf(LOG_INFO, "%04d <%s> %s listing (%ld bytes) of /%s/%s (%lu files) created in %ld seconds"
 						    ,sock, user.alias, cmd, ftell(fp), scfg.lib[lib]->sname, scfg.dir[dir]->code_suffix
-						    ,(ulong)g.gl_pathc, (long)time(NULL) - start);
+						    ,(ulong)file_count, (long)time(NULL) - start);
 					}
-					globfree(&g);
+					freefiles(file_list, file_count);
+					smb_close(&smb);
 				} else 
 					lprintf(LOG_INFO,"%04d <%s> %s listing: /%s/%s directory in %s mode (empty - no access)"
 						,sock, user.alias, cmd, scfg.lib[lib]->sname, scfg.dir[dir]->code_suffix, mode);
@@ -3913,7 +3856,7 @@ static void ctrl_thread(void* arg)
 						if(detail) {
 							if(fexistcase(qwkfile)) {
 								t=fdate(qwkfile);
-								l=flength(qwkfile);
+								l=(ulong)flength(qwkfile);
 							} else {
 								t=time(NULL);
 								l=10240;
@@ -4067,65 +4010,55 @@ static void ctrl_thread(void* arg)
 					,sock, user.alias, detail ? "detailed ":""
 					,scfg.lib[lib]->sname, scfg.dir[dir]->code_suffix, mode);
 
+				smb_t smb;
+				if((result = smb_open_dir(&scfg, &smb, dir)) != SMB_SUCCESS) {
+					lprintf(LOG_ERR, "ERROR %d (%s) opening %s", result, smb.last_error, smb.file);
+					continue;
+				}
 				time_t start = time(NULL);
-				SAFEPRINTF2(path,"%s%s",scfg.dir[dir]->path,filespec);
-				glob(path, GLOB_MARK, NULL, &g);
-				for(i=0;i<(int)g.gl_pathc;i++) {
-					if(*lastchar(g.gl_pathv[i]) == '/')	/* is directory */
-						continue;
-#ifdef _WIN32
-					GetShortPathName(g.gl_pathv[i], str, sizeof(str));
-#else
-					SAFECOPY(str,g.gl_pathv[i]);
-#endif
-					padfname(getfname(str),f.name);
-					f.dir=dir;
-					if((filedat=getfileixb(&scfg,&f))==FALSE
-						&& !(startup->options&FTP_OPT_DIR_FILES)
-						&& !(scfg.dir[dir]->misc&DIR_FILES))
-						continue;
+				size_t file_count = 0;
+				file_t* file_list = loadfiles(&smb
+					,filespec, /* time: */0, file_detail_normal, scfg.dir[dir]->sort, &file_count);
+				for(size_t i = 0; i < file_count; i++) {
+					file_t* f = &file_list[i];
 					if(detail) {
-						if(filedat && !getfiledat(&scfg,&f))
-							continue;
-						f.size = f.cdt;
-						t = f.dateuled;
-						if(!filedat || (scfg.dir[dir]->misc&DIR_FCHK)) {
+						f->size = f->cost;
+						t = f->hdr.when_imported.time;
+						if(scfg.dir[dir]->misc&DIR_FCHK) {
 							struct stat st;
-							if(stat(g.gl_pathv[i], &st) != 0)
+							if(stat(getfilepath(&scfg, f, path), &st) != 0)
 								continue;
-							f.size = st.st_size;
+							f->size = st.st_size;
 							t = st.st_mtime;
 						}
 						if(localtime_r(&t,&tm)==NULL)
 							memset(&tm,0,sizeof(tm));
-						if(filedat) {
-							if(f.misc&FM_ANON)
-								SAFECOPY(str,ANONYMOUS);
-							else
-								dotname(f.uler,str);
-						} else
-							SAFECOPY(str,scfg.sys_id);
-						fprintf(fp,"-r--r--r--   1 %-*s %-8s %9"PRId32" %s %2d "
+						if(f->hdr.attr & MSG_ANONYMOUS)
+							SAFECOPY(str,ANONYMOUS);
+						else
+							dotname(f->from,str);
+						fprintf(fp,"-r--r--r--   1 %-*s %-8s %9"PRId64" %s %2d "
 							,NAME_LEN
 							,str
 							,scfg.dir[dir]->code_suffix
-							,f.size
+							,(int64_t)f->size
 							,ftp_mon[tm.tm_mon],tm.tm_mday);
 						if(tm.tm_year==cur_tm.tm_year)
 							fprintf(fp,"%02d:%02d %s\r\n"
 								,tm.tm_hour,tm.tm_min
-								,getfname(g.gl_pathv[i]));
+								,f->name);
 						else
 							fprintf(fp,"%5d %s\r\n"
 								,1900+tm.tm_year
-								,getfname(g.gl_pathv[i]));
+								,f->name);
 					} else
-						fprintf(fp,"%s\r\n",getfname(g.gl_pathv[i]));
+						fprintf(fp,"%s\r\n", f->name);
 				}
 				lprintf(LOG_INFO, "%04d <%s> %slisting (%ld bytes) of /%s/%s (%lu files) created in %ld seconds"
 					,sock, user.alias, detail ? "detailed ":"", ftell(fp), scfg.lib[lib]->sname, scfg.dir[dir]->code_suffix
-					,(ulong)g.gl_pathc, (long)time(NULL) - start);
-				globfree(&g);
+					,(ulong)file_count, (long)time(NULL) - start);
+				freefiles(file_list, file_count);
+				smb_close(&smb);
 			} else
 				lprintf(LOG_INFO,"%04d <%s> %slisting: /%s/%s directory in %s mode (empty - no access)"
 					,sock, user.alias, detail ? "detailed ":"", scfg.lib[lib]->sname, scfg.dir[dir]->code_suffix, mode);
@@ -4366,34 +4299,25 @@ static void ctrl_thread(void* arg)
 								,INDEX_FNAME_LEN,scfg.dir[i]->code_suffix,scfg.dir[i]->lname);
 						}
 					} else if(chk_ar(&scfg,scfg.dir[dir]->ar,&user,&client)){
-						sprintf(cmd,"%s*",scfg.dir[dir]->path);
+						smb_t smb;
+						if((result = smb_open_dir(&scfg, &smb, dir)) != SMB_SUCCESS) {
+							lprintf(LOG_ERR, "ERROR %d (%s) opening %s", result, smb.last_error, smb.file);
+							continue;
+						}
 						time_t start = time(NULL);
-						glob(cmd, GLOB_MARK, NULL, &g);
-						for(i=0;i<(int)g.gl_pathc;i++) {
-							if(*lastchar(g.gl_pathv[i]) == '/')	/* is directory */
-								continue;
-	#ifdef _WIN32
-							GetShortPathName(g.gl_pathv[i], str, sizeof(str));
-	#else
-							SAFECOPY(str,g.gl_pathv[i]);
-	#endif
-							memset(&f, 0, sizeof(f));
-							padfname(getfname(str),f.name);
-							f.dir=dir;
-							if((filedat=getfileixb(&scfg,&f))==FALSE
-								&& !(startup->options&FTP_OPT_DIR_FILES)
-								&& !(scfg.dir[dir]->misc&DIR_FILES))
-								continue;
-							f.size = -1;	// Not used, don't query
-							if(filedat && !getfiledat(&scfg,&f))
-								continue;
+						size_t file_count = 0;
+						file_t* file_list = loadfiles(&smb
+							,/* filespec */NULL, /* time: */0, file_detail_normal, scfg.dir[dir]->sort, &file_count);
+						for(size_t i = 0; i < file_count; i++) {
+							file_t* f = &file_list[i];
 							fprintf(fp,"%-*s %s\r\n",INDEX_FNAME_LEN
-								,getfname(g.gl_pathv[i]),f.desc);
+								,f->name, f->desc);
 						}
 						lprintf(LOG_INFO, "%04d <%s> index (%ld bytes) of /%s/%s (%lu files) created in %ld seconds"
 							,sock, user.alias, ftell(fp), scfg.lib[lib]->sname, scfg.dir[dir]->code_suffix
-							,(ulong)g.gl_pathc, (long)time(NULL) - start);
-						globfree(&g);
+							,(ulong)file_count, (long)time(NULL) - start);
+						freefiles(file_list, file_count);
+						smb_close(&smb);
 					}
 					fclose(fp);
 				}
@@ -4430,17 +4354,8 @@ static void ctrl_thread(void* arg)
 					continue;
 				}
 				SAFEPRINTF2(fname,"%s%s",scfg.dir[dir]->path,p);
-#ifdef _WIN32
-				GetShortPathName(fname, str, sizeof(str));
-#else
-				SAFECOPY(str,fname);
-#endif
-				padfname(getfname(str),f.name);
-				f.dir=dir;
-				f.cdt=0;
-				f.size=-1;
-				filedat=getfileixb(&scfg,&f);
-				if(!filedat && !(startup->options&FTP_OPT_DIR_FILES) && !(scfg.dir[dir]->misc&DIR_FILES)) {
+				filedat=findfile(&scfg, dir, p, NULL);
+				if(!filedat) {
 					sockprintf(sock,sess,"550 File not found: %s",p);
 					lprintf(LOG_WARNING,"%04d <%s> file (%s%s) not in database for %.4s command"
 						,sock,user.alias,genvpath(lib,dir,str),p,cmd);
@@ -4451,20 +4366,23 @@ static void ctrl_thread(void* arg)
 				/* Verify credits */
 				if(!getsize && !getdate && !delecmd
 					&& !is_download_free(&scfg,dir,&user,&client)) {
+					file_t f;
 					if(filedat)
-						getfiledat(&scfg,&f);
+						loadfile(&scfg, dir, p, &f, file_detail_normal);
 					else
-						f.cdt=flength(fname);
-					if(f.cdt>(user.cdt+user.freecdt)) {
+						f.cost=(uint32_t)flength(fname);
+					if(f.cost>(user.cdt+user.freecdt)) {
 						lprintf(LOG_WARNING,"%04d <%s> has insufficient credit to download /%s/%s/%s (%lu credits)"
 							,sock,user.alias,scfg.lib[scfg.dir[dir]->lib]->sname
 							,scfg.dir[dir]->code_suffix
 							,p
-							,(ulong)f.cdt);
-						sockprintf(sock,sess,"550 Insufficient credit (%lu required).", (ulong)f.cdt);
+							,(ulong)f.cost);
+						sockprintf(sock,sess,"550 Insufficient credit (%lu required).", (ulong)f.cost);
 						filepos=0;
+						smb_freefilemem(&f);
 						continue;
 					}
+					smb_freefilemem(&f);
 				}
 
 				if(strcspn(p,ILLEGAL_FILENAME_CHARS)!=strlen(p)) {
@@ -4504,7 +4422,7 @@ static void ctrl_thread(void* arg)
 				} else {
 					lprintf(LOG_NOTICE,"%04d <%s> deleted %s",sock,user.alias,fname);
 					if(filedat) 
-						removefiledat(&scfg,&f);
+						removefile(&scfg, dir, getfname(fname));
 					sockprintf(sock,sess,"250 %s deleted.",fname);
 				}
 			} else if(success) {
@@ -4659,16 +4577,8 @@ static void ctrl_thread(void* arg)
 					continue;
 				}
 				if(append || filepos) {	/* RESUME */
-#ifdef _WIN32
-					GetShortPathName(fname, str, sizeof(str));
-#else
-					SAFECOPY(str,fname);
-#endif
-					padfname(getfname(str),f.name);
-					f.dir=dir;
-					f.cdt=0;
-					f.size=-1;
-					if(!getfileixb(&scfg,&f) || !getfiledat(&scfg,&f)) {
+					file_t f;
+					if(!loadfile(&scfg, dir, p, &f, file_detail_normal)) {
 						if(filepos) {
 							lprintf(LOG_WARNING,"%04d <%s> file (%s) not in database for %.4s command"
 								,sock,user.alias,fname,cmd);
@@ -4678,12 +4588,14 @@ static void ctrl_thread(void* arg)
 						append=FALSE;
 					}
 					/* Verify user is original uploader */
-					if((append || filepos) && stricmp(f.uler,user.alias)) {
+					if((append || filepos) && stricmp(f.from, user.alias)) {
 						lprintf(LOG_WARNING,"%04d <%s> !cannot resume upload of %s, uploaded by %s"
-							,sock,user.alias,fname,f.uler);
+							,sock,user.alias,fname,f.from);
 						sockprintf(sock,sess,"553 Insufficient access (can't resume upload from different user).");
+						smb_freefilemem(&f);
 						continue;
 					}
+					smb_freefilemem(&f);
 				}
 				lprintf(LOG_INFO,"%04d <%s> uploading: %s to %s (%s) in %s mode"
 					,sock,user.alias
@@ -4990,7 +4902,7 @@ const char* DLLCALL ftp_ver(void)
 #else
 		,""
 #endif
-		,git_branch, git_hash
+		,GIT_BRANCH, GIT_HASH
 		,__DATE__, __TIME__, compiler);
 
 	return(ver);
@@ -5072,7 +4984,7 @@ void DLLCALL ftp_server(void* arg)
 
 		DESCRIBE_COMPILER(compiler);
 
-		lprintf(LOG_INFO,"Compiled %s/%s %s %s with %s", git_branch, git_hash, __DATE__, __TIME__, compiler);
+		lprintf(LOG_INFO,"Compiled %s/%s %s %s with %s", GIT_BRANCH, GIT_HASH, __DATE__, __TIME__, compiler);
 
 		sbbs_srand();	/* Seed random number generator */
 
diff --git a/src/sbbs3/ftpsrvr.h b/src/sbbs3/ftpsrvr.h
index 028e57dc7fe6c9c81d81acab09edf0f7e336b364..acf8c11b691e2d386d6a3941380d73dbadcfda6b 100644
--- a/src/sbbs3/ftpsrvr.h
+++ b/src/sbbs3/ftpsrvr.h
@@ -43,8 +43,8 @@ typedef struct {
 	WORD	pasv_port_low;
 	WORD	pasv_port_high;
     DWORD	options;			/* See FTP_OPT definitions */
-	uint64_t	min_fsize;		/* Minimum file size accepted for upload */
-	uint64_t	max_fsize;		/* Maximum file size accepted for upload (0=unlimited) */
+	int64_t	min_fsize;			/* Minimum file size accepted for upload */
+	int64_t	max_fsize;			/* Maximum file size accepted for upload (0=unlimited) */
 
 	void*	cbdata;				/* Private data passed to callbacks */ 
 
diff --git a/src/sbbs3/ftpsrvr.vcxproj b/src/sbbs3/ftpsrvr.vcxproj
index 7bcbbde8917079093e640d756a0de47baefb822c..84539c12fdf751e01c69ae5e86e8d4877fb329cb 100644
--- a/src/sbbs3/ftpsrvr.vcxproj
+++ b/src/sbbs3/ftpsrvr.vcxproj
@@ -176,7 +176,6 @@
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
     </ClCompile>
-    <ClCompile Include="ver.cpp" />
   </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\xpdev\xpdev_mt.vcxproj">
diff --git a/src/sbbs3/getmsg.cpp b/src/sbbs3/getmsg.cpp
index 7b59ce3f1155b09690ffd1e4ecbc96b2206416e8..ae7c707c703d448b2c3da14ba87155acd6bcf825 100644
--- a/src/sbbs3/getmsg.cpp
+++ b/src/sbbs3/getmsg.cpp
@@ -403,10 +403,7 @@ void sbbs_t::download_msg_attachments(smb_t* smb, smbmsg_t* msg, bool del)
 			p=strchr(tp,' ');
 			if(p) *p=0;
 			tp=getfname(tp);
-			file_t	fd;
-			fd.dir=cfg.total_dirs+1;			/* temp dir for file attachments */
 			if(strcspn(tp, ILLEGAL_FILENAME_CHARS) == strlen(tp)) {
-				padfname(tp,fd.name);
 				SAFEPRINTF3(fpath,"%sfile/%04u.in/%s"  /* path is path/fname */
 					,cfg.data_dir, msg->idx.to, tp);
 				if(!fexistcase(fpath) && msg->idx.from)
@@ -441,7 +438,7 @@ void sbbs_t::download_msg_attachments(smb_t* smb, smbmsg_t* msg, bool del)
 									break;
 							if(i<cfg.total_prots) {
 								int error = protocol(cfg.prot[i], XFER_DOWNLOAD, fpath, nulstr, false);
-								if(checkprotresult(cfg.prot[i],error,&fd)) {
+								if(checkprotresult(cfg.prot[i],error,fpath)) {
 									if(del)
 										(void)remove(fpath);
 									logon_dlb+=length;	/* Update stats */
@@ -451,10 +448,10 @@ void sbbs_t::download_msg_attachments(smb_t* smb, smbmsg_t* msg, bool del)
 									useron.dlb=adjustuserrec(&cfg,useron.number
 										,U_DLB,10,length);
 									bprintf(text[FileNBytesSent]
-										,fd.name,ultoac(length,tmp));
+										,getfname(fpath),ultoac(length,tmp));
 									SAFEPRINTF(str
 										,"downloaded attached file: %s"
-										,fd.name);
+										,getfname(fpath));
 									logline("D-",str);
 								}
 								autohangup();
@@ -569,7 +566,7 @@ time_t sbbs_t::getmsgtime(uint subnum, ulong ptr)
 		smb_close(&smb);
 		return(0);
 	}
-	msg.offset=0;
+	msg.idx_offset=0;
 	msg.hdr.number=0;
 	if(smb_getmsgidx(&smb,&msg)) {				/* Get first message index */
 		smb_close(&smb);
@@ -597,13 +594,13 @@ time_t sbbs_t::getmsgtime(uint subnum, ulong ptr)
 	}
 
 	if(ptr-msg.idx.number < lastidx.number-ptr) {
-		msg.offset=0;
+		msg.idx_offset=0;
 		msg.idx.number=0;
 		while(msg.idx.number<ptr) {
 			msg.hdr.number=0;
 			if(smb_getmsgidx(&smb,&msg) || msg.idx.number>=ptr)
 				break;
-			msg.offset++;
+			msg.idx_offset++;
 		}
 		smb_close(&smb);
 		return(msg.idx.time);
diff --git a/src/sbbs3/getstats.c b/src/sbbs3/getstats.c
index 8de3c082e22a00f1b58eb9299f0a287e1d6d7cd8..de5aa52c6029166d76c8b8b668a6ddb0749cf296 100644
--- a/src/sbbs3/getstats.c
+++ b/src/sbbs3/getstats.c
@@ -48,16 +48,16 @@ BOOL DLLCALL getstats(scfg_t* cfg, char node, stats_t* stats)
 /****************************************************************************/
 long DLLCALL getfiles(scfg_t* cfg, uint dirnum)
 {
-	char str[256];
-	long l;
+	char path[MAX_PATH + 1];
+	off_t l;
 
-	if(dirnum>=cfg->total_dirs)	/* out of range */
-		return(0);
-	sprintf(str,"%s%s.ixb",cfg->dir[dirnum]->data_dir, cfg->dir[dirnum]->code);
-	l=(long)flength(str);
-	if(l>0L)
-		return(l/F_IXBSIZE);
-	return(0);
+	if(dirnum >= cfg->total_dirs)	/* out of range */
+		return 0;
+	SAFEPRINTF2(path, "%s%s.sid", cfg->dir[dirnum]->data_dir, cfg->dir[dirnum]->code);
+	l = flength(path);
+	if(l <= 0)
+		return 0;
+	return (long)(l / sizeof(fileidxrec_t));
 }
 
 /****************************************************************************/
@@ -73,7 +73,7 @@ ulong DLLCALL getposts(scfg_t* cfg, uint subnum)
 		l = flength(path);
 		if(l < sizeof(idxrec_t))
 			return 0;
-		return l / sizeof(idxrec_t);
+		return (ulong)(l / sizeof(idxrec_t));
 	}
 	smb_t smb = {{0}};
 	SAFEPRINTF2(smb.file, "%s%s", cfg->sub[subnum]->data_dir, cfg->sub[subnum]->code);
diff --git a/src/sbbs3/js_archive.c b/src/sbbs3/js_archive.c
new file mode 100644
index 0000000000000000000000000000000000000000..459b09713a8d171fd9a91f44dd495999c8edc3e4
--- /dev/null
+++ b/src/sbbs3/js_archive.c
@@ -0,0 +1,674 @@
+/* Synchronet JavaScript "Archive" Object */
+
+/****************************************************************************
+ * @format.tab-size 4		(Plain Text/Source Code File Header)			*
+ * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
+ *																			*
+ * Copyright Rob Swindell - http://www.synchro.net/copyright.html			*
+ *																			*
+ * 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.					*
+ * See the GNU General Public License for more details: gpl.txt or			*
+ * http://www.fsf.org/copyleft/gpl.html										*
+ *																			*
+ * For Synchronet coding style and modification guidelines, see				*
+ * http://www.synchro.net/source.html										*
+ *																			*
+ * Note: If this box doesn't appear square, then you need to fix your tabs.	*
+ ****************************************************************************/
+
+#include "sbbs.h"
+#include "js_request.h"
+#include "filedat.h"
+
+/* libarchive: */
+#include <archive.h>
+#include <archive_entry.h>
+
+#include <stdbool.h>
+
+JSClass js_archive_class;
+
+static JSBool
+js_create(JSContext *cx, uintN argc, jsval *arglist)
+{
+	jsval *argv = JS_ARGV(cx, arglist);
+	JSObject *obj = JS_THIS_OBJECT(cx, arglist);
+	char		format[32] = "";
+	str_list_t	file_list = NULL;
+	char		path[MAX_PATH + 1];
+	bool		with_path = false;
+	char		error[256] = "";
+	jsval		val;
+	jsrefcount	rc;
+	const char* filename;
+
+	if((filename = js_GetClassPrivate(cx, obj, &js_archive_class)) == NULL)
+		return JS_FALSE;
+
+	if(!js_argc(cx, argc, 1))
+		return JS_FALSE;
+
+	uintN argn = 0;
+	if(argn < argc && JSVAL_IS_STRING(argv[argn])) {
+		JSString* js_str = JS_ValueToString(cx, argv[argn]);
+		if(js_str == NULL) {
+			JS_ReportError(cx, "string conversion error");
+			return JS_FALSE;
+		}
+		JSSTRING_TO_STRBUF(cx, js_str, format, sizeof(format), NULL);
+		argn++;
+	}
+	if(argc > argn && JSVAL_IS_BOOLEAN(argv[argn])) {
+		with_path = JSVAL_TO_BOOLEAN(argv[argn]);
+		argn++;
+	}
+	if(argn < argc && JSVAL_IS_OBJECT(argv[argn])) {
+		JSObject* array = JSVAL_TO_OBJECT(argv[argn]);
+		if(!JS_IsArrayObject(cx, array)) {
+			JS_ReportError(cx, "invalid array object");
+			return JS_FALSE;
+		}
+		file_list = strListInit();
+		for(jsint i = 0;; i++)  {
+			if(!JS_GetElement(cx, array, i, &val))
+				break;
+			if(!JSVAL_IS_STRING(val))
+				break;
+			JSVALUE_TO_STRBUF(cx, val, path, sizeof(path), NULL);
+			strListPush(&file_list, path);
+		}
+		argn++;
+	}
+
+	char* fext;
+	if(*format == '\0' && (fext = getfext(filename)) != NULL)
+		SAFECOPY(format,  fext + 1);
+	if(*format == '\0')
+		SAFECOPY(format, "zip");
+	rc = JS_SUSPENDREQUEST(cx);
+	long file_count = create_archive(filename, format, with_path, file_list, error, sizeof(error));
+	strListFree(&file_list);
+	JS_RESUMEREQUEST(cx, rc);
+	if(file_count < 0) {
+		JS_ReportError(cx, error);
+		return JS_FALSE;
+	}
+	JS_SET_RVAL(cx, arglist, INT_TO_JSVAL(file_count));
+	return JS_TRUE;
+}
+
+static JSBool
+js_extract(JSContext *cx, uintN argc, jsval *arglist)
+{
+	jsval *argv = JS_ARGV(cx, arglist);
+	JSObject *obj = JS_THIS_OBJECT(cx, arglist);
+	char*		outdir = NULL;
+	char*		allowed_filename_chars = SAFEST_FILENAME_CHARS;
+	str_list_t	file_list = NULL;
+	bool		with_path = false;
+	int32		max_files = 0;
+	char		error[256] = "";
+	jsrefcount	rc;
+	const char* filename;
+	JS_SET_RVAL(cx, arglist, JSVAL_VOID);
+
+	if((filename = js_GetClassPrivate(cx, obj, &js_archive_class)) == NULL)
+		return JS_FALSE;
+
+ 	if(!js_argc(cx, argc, 1))
+		return JS_FALSE;
+
+	if(JSVAL_NULL_OR_VOID(argv[0]))
+		JS_ReportError(cx, "Invalid output directory specified (null or undefined)");
+	else
+		JSVALUE_TO_MSTRING(cx, argv[0], outdir, NULL);
+	if(JS_IsExceptionPending(cx)) {
+		free(outdir);
+		return JS_FALSE;
+	}
+	uintN argn = 1;
+	if(argc > argn && JSVAL_IS_BOOLEAN(argv[argn])) {
+		with_path = JSVAL_TO_BOOLEAN(argv[argn]);
+		if(with_path)
+			allowed_filename_chars = NULL;	// We trust this archive
+		argn++;
+	}
+	if(argc > argn && JSVAL_IS_NUMBER(argv[argn])) {
+		if(!JS_ValueToInt32(cx, argv[argn], &max_files)) {
+			free(outdir);
+			strListFree(&file_list);
+			return JS_FALSE;
+		}
+		argn++;
+	}
+	for(; argn < argc; argn++) {
+		if(JSVAL_IS_STRING(argv[argn])) {
+			char path[MAX_PATH + 1];
+			JSVALUE_TO_STRBUF(cx, argv[argn], path, sizeof(path), NULL);
+			strListPush(&file_list, path);
+		}
+	}
+
+	rc = JS_SUSPENDREQUEST(cx);
+	long extracted = extract_files_from_archive(filename, outdir, allowed_filename_chars
+		,with_path, (ulong)max_files, file_list, error, sizeof(error));
+	strListFree(&file_list);
+	free(outdir);
+	JS_RESUMEREQUEST(cx, rc);
+	if(*error != '\0') {
+		if(extracted >= 0)
+			JS_ReportError(cx, "%s (after extracting %ld items successfully)", error, extracted);
+		else
+			JS_ReportError(cx, "%s", error);
+		return JS_FALSE;
+	}
+	JS_SET_RVAL(cx, arglist, INT_TO_JSVAL(extracted));
+	return JS_TRUE;
+}
+
+/* getter */
+static JSBool
+js_archive_type(JSContext *cx, JSObject *obj, jsid id, jsval *vp)
+{
+	char		type[256] = "";
+	jsrefcount	rc;
+	const char* filename;
+
+	if((filename = js_GetClassPrivate(cx, obj, &js_archive_class)) == NULL)
+		return JS_FALSE;
+
+	rc = JS_SUSPENDREQUEST(cx);
+	int result = archive_type(filename, type, sizeof(type));
+	JS_RESUMEREQUEST(cx, rc);
+	if(result >= 0) {
+		JSString* js_str = JS_NewStringCopyZ(cx, type);
+		*vp = STRING_TO_JSVAL(js_str);
+	} else
+		*vp = JSVAL_NULL;
+	return JS_TRUE;
+}
+
+// TODO: consider making 'path' and 'case-sensitive' arguments to wildmatch() configurable via method arguments
+static JSBool
+js_list(JSContext *cx, uintN argc, jsval *arglist)
+{
+	jsval *argv = JS_ARGV(cx, arglist);
+	JSObject *obj = JS_THIS_OBJECT(cx, arglist);
+	jsval val;
+	jsrefcount	rc;
+	JSObject* array;
+	JSString* js_str;
+	struct archive *ar;
+	struct archive_entry *entry;
+	bool hash = false;
+	int	result;
+	const char* filename;
+	char pattern[MAX_PATH + 1] = "";
+
+	if((filename = js_GetClassPrivate(cx, obj, &js_archive_class)) == NULL)
+		return JS_FALSE;
+
+	uintN argn = 0;
+	if(argc > argn && JSVAL_IS_BOOLEAN(argv[argn])) {
+		hash = JSVAL_TO_BOOLEAN(argv[argn]);
+		argn++;
+	}
+	if(argc > argn && JSVAL_IS_STRING(argv[argn])) {
+		JSString* js_str = JS_ValueToString(cx, argv[argn]);
+		if(js_str == NULL) {
+			JS_ReportError(cx, "string conversion error");
+			return JS_FALSE;
+		}
+		JSSTRING_TO_STRBUF(cx, js_str, pattern, sizeof(pattern), NULL);
+		argn++;
+	}
+
+	if((ar = archive_read_new()) == NULL) {
+		JS_ReportError(cx, "archive_read_new() returned NULL");
+		return JS_FALSE;
+	}
+	archive_read_support_filter_all(ar);
+	archive_read_support_format_all(ar);
+	if((result = archive_read_open_filename(ar, filename, 10240)) != ARCHIVE_OK) {
+		JS_ReportError(cx, "archive_read_open_filename() returned %d: %s"
+			,result, archive_error_string(ar));
+		archive_read_free(ar);
+		return JS_FALSE;
+	}
+
+    if((array = JS_NewArrayObject(cx, 0, NULL))==NULL) {
+		JS_ReportError(cx, "JS_NewArrayObject() returned NULL");
+		archive_read_free(ar);
+		return JS_FALSE;
+	}
+
+	JSBool retval = JS_TRUE;
+	rc = JS_SUSPENDREQUEST(cx);
+	jsint len = 0;
+	while(1) {
+		result = archive_read_next_header(ar, &entry);
+		if(result != ARCHIVE_OK) {
+			if(result != ARCHIVE_EOF) {
+				JS_ReportError(cx, "archive_read_next_header() returned %d: %s"
+					,result, archive_error_string(ar));
+				retval = JS_FALSE;
+			}
+			break;
+		}
+
+		const char* p = archive_entry_pathname(entry);
+		if(p == NULL)
+			continue;
+
+		if(*pattern && !wildmatch(p, pattern, /* path: */false, /* case-sensitive: */false))
+			continue;
+
+		const char* type;
+		switch(archive_entry_filetype(entry)) {
+			case AE_IFREG:
+				type = "file";
+				break;
+			case AE_IFLNK:
+				type = "link";
+				break;
+			case AE_IFDIR:
+				type = "directory";
+				break;
+			default:
+				continue;
+		}
+
+		JSObject* obj = JS_NewObject(cx, NULL, NULL, NULL);
+		if(obj == NULL)
+			break;
+
+		js_str = JS_NewStringCopyZ(cx, type);
+		if(js_str == NULL)
+			break;
+		val = STRING_TO_JSVAL(js_str);
+		JS_SetProperty(cx, obj, "type", &val);
+
+		js_str = JS_NewStringCopyZ(cx, p);
+		if(js_str == NULL)
+			break;
+		val = STRING_TO_JSVAL(js_str);
+		JS_SetProperty(cx, obj, "name", &val);
+
+		if((p = archive_entry_sourcepath(entry)) != NULL) {
+			js_str = JS_NewStringCopyZ(cx, p);
+			if(js_str == NULL)
+				break;
+			val = STRING_TO_JSVAL(js_str);
+			JS_SetProperty(cx, obj, "path", &val);
+		}
+
+		if((p = archive_entry_symlink(entry)) != NULL) {
+			js_str = JS_NewStringCopyZ(cx, p);
+			if(js_str == NULL)
+				break;
+			val = STRING_TO_JSVAL(js_str);
+			JS_SetProperty(cx, obj, "symlink", &val);
+
+//			val = INT_TO_JSVAL(archive_entry_symlink_type(entry));
+//			JS_SetProperty(cx, obj, "symlink_type", &val);
+		}
+
+		if((p = archive_entry_hardlink(entry)) != NULL) {
+			js_str = JS_NewStringCopyZ(cx, p);
+			if(js_str == NULL)
+				break;
+			val = STRING_TO_JSVAL(js_str);
+			JS_SetProperty(cx, obj, "hardlink", &val);
+		}
+
+		val = DOUBLE_TO_JSVAL((jsdouble)archive_entry_size(entry));
+		JS_SetProperty(cx, obj, "size", &val);
+
+		val = DOUBLE_TO_JSVAL((jsdouble)archive_entry_mtime(entry));
+		JS_SetProperty(cx, obj, "time", &val);
+
+		val = INT_TO_JSVAL(archive_entry_mode(entry));
+		JS_SetProperty(cx, obj, "mode", &val);
+
+		if((p = archive_entry_uname(entry)) != NULL) {
+			js_str = JS_NewStringCopyZ(cx, p);
+			if(js_str == NULL)
+				break;
+			val = STRING_TO_JSVAL(js_str);
+			JS_SetProperty(cx, obj, "user", &val);
+		}
+
+		if((p = archive_entry_gname(entry)) != NULL) {
+			js_str = JS_NewStringCopyZ(cx, p);
+			if(js_str == NULL)
+				break;
+			val = STRING_TO_JSVAL(js_str);
+			JS_SetProperty(cx, obj, "group", &val);
+		}
+
+		if((p = archive_format_name(ar)) != NULL) {
+			js_str = JS_NewStringCopyZ(cx, p);
+			if(js_str == NULL)
+				break;
+			val = STRING_TO_JSVAL(js_str);
+			JS_SetProperty(cx, obj, "format", &val);
+		}
+
+		if((p = archive_filter_name(ar, 0)) != NULL) {
+			js_str = JS_NewStringCopyZ(cx, p);
+			if(js_str == NULL)
+				break;
+			val = STRING_TO_JSVAL(js_str);
+			JS_SetProperty(cx, obj, "compression", &val);
+		}
+
+		if((p = archive_entry_fflags_text(entry)) != NULL) {
+			js_str = JS_NewStringCopyZ(cx, p);
+			if(js_str == NULL)
+				break;
+			val = STRING_TO_JSVAL(js_str);
+			JS_SetProperty(cx, obj, "fflags", &val);
+		}
+
+		if(hash && archive_entry_filetype(entry) == AE_IFREG) {
+			MD5 md5_ctx;
+			SHA1_CTX sha1_ctx;
+			uint8_t md5[MD5_DIGEST_SIZE];
+			uint8_t sha1[SHA1_DIGEST_SIZE];
+			uint16_t crc16 = 0;
+			uint32_t crc32 = 0;
+
+			MD5_open(&md5_ctx);
+			SHA1Init(&sha1_ctx);
+
+			const void *buff;
+			size_t size;
+			la_int64_t offset;
+
+			for(;;) {
+				result = archive_read_data_block(ar, &buff, &size, &offset);
+				if(result != ARCHIVE_OK)
+					break;
+				crc32 = crc32i(~crc32, buff, size);
+				crc16 = icrc16(crc16, buff, size);
+				MD5_digest(&md5_ctx, buff, size);
+				SHA1Update(&sha1_ctx, buff, size);
+			}
+			MD5_close(&md5_ctx, md5);
+			SHA1Final(&sha1_ctx, sha1);
+			val = UINT_TO_JSVAL(crc16);
+			if(!JS_SetProperty(cx, obj, "crc16", &val))
+				break;
+			val = UINT_TO_JSVAL(crc32);
+			if(!JS_SetProperty(cx, obj, "crc32", &val))
+				break;
+			char hex[128];
+			if((js_str = JS_NewStringCopyZ(cx, MD5_hex(hex, md5))) == NULL)
+				break;
+			val = STRING_TO_JSVAL(js_str);
+			if(!JS_SetProperty(cx, obj, "md5", &val))
+				break;
+			if((js_str = JS_NewStringCopyZ(cx, SHA1_hex(hex, sha1))) == NULL)
+				break;
+			val = STRING_TO_JSVAL(js_str);
+			if(!JS_SetProperty(cx, obj, "sha1", &val))
+				break;
+		}
+
+		val = OBJECT_TO_JSVAL(obj);
+        if(!JS_SetElement(cx, array, len++, &val))
+			break;
+	}
+	archive_read_free(ar);
+	JS_RESUMEREQUEST(cx, rc);
+	JS_SET_RVAL(cx, arglist, OBJECT_TO_JSVAL(array));
+
+	return retval;
+}
+
+static JSBool
+js_read(JSContext *cx, uintN argc, jsval *arglist)
+{
+	jsval *argv = JS_ARGV(cx, arglist);
+	JSObject *obj = JS_THIS_OBJECT(cx, arglist);
+	jsrefcount	rc;
+	struct archive *ar;
+	struct archive_entry *entry;
+	int	result;
+	const char* filename;
+	char pattern[MAX_PATH + 1] = "";
+
+	if((filename = js_GetClassPrivate(cx, obj, &js_archive_class)) == NULL)
+		return JS_FALSE;
+
+ 	if(!js_argc(cx, argc, 1))
+		return JS_FALSE;
+
+	JS_SET_RVAL(cx, arglist, JSVAL_NULL);
+
+	uintN argn = 0;
+	if(argc > argn && JSVAL_IS_STRING(argv[argn])) {
+		JSString* js_str = JS_ValueToString(cx, argv[argn]);
+		if(js_str == NULL) {
+			JS_ReportError(cx, "string conversion error");
+			return JS_FALSE;
+		}
+		JSSTRING_TO_STRBUF(cx, js_str, pattern, sizeof(pattern), NULL);
+		argn++;
+	}
+
+	if((ar = archive_read_new()) == NULL) {
+		JS_ReportError(cx, "archive_read_new() returned NULL");
+		return JS_FALSE;
+	}
+	archive_read_support_filter_all(ar);
+	archive_read_support_format_all(ar);
+	if((result = archive_read_open_filename(ar, filename, 10240)) != ARCHIVE_OK) {
+		JS_ReportError(cx, "archive_read_open_filename() returned %d: %s"
+			,result, archive_error_string(ar));
+		archive_read_free(ar);
+		return JS_FALSE;
+	}
+
+	JSBool retval = JS_TRUE;
+	rc = JS_SUSPENDREQUEST(cx);
+	while(1) {
+		result = archive_read_next_header(ar, &entry);
+		if(result != ARCHIVE_OK) {
+			if(result != ARCHIVE_EOF) {
+				JS_ReportError(cx, "archive_read_next_header() returned %d: %s"
+					,result, archive_error_string(ar));
+				retval = JS_FALSE;
+			}
+			break;
+		}
+
+		if(archive_entry_filetype(entry) != AE_IFREG)
+			continue;
+
+		const char* pathname = archive_entry_pathname(entry);
+		if(pathname == NULL)
+			continue;
+
+		if(stricmp(pathname, pattern) != 0)
+			continue;
+
+		char* p = NULL;
+		size_t total = 0;
+		const void *buff;
+		size_t size;
+		la_int64_t offset;
+
+		for(;;) {
+			result = archive_read_data_block(ar, &buff, &size, &offset);
+			if(result != ARCHIVE_OK)
+				break;
+			char* np = realloc(p, total + size);
+			if(np == NULL)
+				break;
+			p = np;
+			memcpy(p + total, buff, size);
+			total += size;
+		}
+		JSString* js_str = JS_NewStringCopyN(cx, p, total);
+		JS_SET_RVAL(cx, arglist, STRING_TO_JSVAL(js_str));
+		free(p);
+		break;
+	}
+	archive_read_free(ar);
+	JS_RESUMEREQUEST(cx, rc);
+
+	return retval;
+}
+
+static jsSyncMethodSpec js_archive_functions[] = {
+	{ "create",		js_create,		1,	JSTYPE_NUMBER
+		,JSDOCSTR("[string format] [,boolean with_path = false] [,array file_list]")
+		,JSDOCSTR("create an archive of the specified format, returns the number of files archived, will throw exception upon error")
+		,31900
+	},
+	{ "read",		js_read,		1,	JSTYPE_STRING
+		,JSDOCSTR("string path/filename")
+		,JSDOCSTR("read and return the contents of the specified archived text file")
+		,31900
+	},
+	{ "extract",	js_extract,		1,	JSTYPE_NUMBER
+		,JSDOCSTR("output_directory [,boolean with_path = false] [,number max_files = 0] [,string file/pattern [...]]")
+		,JSDOCSTR("extract files from an archive to specified output directory, returns the number of files extracted, will throw exception upon error")
+		,31900
+	},
+	{ "list",		js_list,		1,	JSTYPE_ARRAY
+		,JSDOCSTR("[,boolean hash = false] [,string file/pattern]")
+		,JSDOCSTR("get list of archive contents as an array of objects<br>"
+			"archived object properties:<br>"
+			"<ul>"
+			"<li>string <tt>type</tt> - item type: 'file', 'link', or 'directory'"
+			"<li>string <tt>name</tt> - item path/name"
+			"<li>string <tt>path</tt> - source path"
+			"<li>string <tt>symlink</tt>"
+			"<li>string <tt>hardlink</tt>"
+			"<li>number <tt>size</tt> - item size in bytes"
+			"<li>number <tt>time</tt> - modification date/time in time_t format"
+			"<li>number <tt>mode</tt> - permissions/mode flags"
+			"<li>string <tt>user</tt> - owner name"
+			"<li>string <tt>group</tt> - owner group"
+			"<li>string <tt>format</tt> - archive format"
+			"<li>string <tt>compression</tt> - compression method"
+			"<li>string <tt>fflags</tt>"
+			"<li>number <tt>crc16</tt> - 16-bit CRC, when hash is true and type is file"
+			"<li>number <tt>crc32</tt> - 32-bit CRC, when hash is true and type is file"
+			"<li>string <tt>md5</tt> - hexadecimal MD-5 sum, when hash is true and type is file"
+			"<li>string <tt>sha1</tt> - hexadecimal SHA-1 sum, when hash is true and type is file"
+			"</ul>"
+			"when <tt>hash</tt> is <tt>true</tt>, calculates and returns hash/digest values of files in stored archive")
+		,31900
+	},
+	{0}
+};
+
+#ifdef BUILD_JSDOCS
+static char* archive_prop_desc[] = {
+
+	 "format/compression type of archive file - <small>READ ONLY</small>"
+	,NULL
+};
+#endif
+
+static JSBool
+js_archive_constructor(JSContext *cx, uintN argc, jsval *arglist)
+{
+	JSObject *obj = JS_THIS_OBJECT(cx, arglist);
+	jsval *argv = JS_ARGV(cx, arglist);
+	JSString* str;
+	char* filename;
+
+	obj = JS_NewObject(cx, &js_archive_class, NULL, NULL);
+	JS_SET_RVAL(cx, arglist, OBJECT_TO_JSVAL(obj));
+	if(argc < 1 || (str = JS_ValueToString(cx, argv[0]))==NULL) {
+		JS_ReportError(cx, "No filename specified");
+		return JS_FALSE;
+	}
+
+	JSSTRING_TO_MSTRING(cx, str, filename, NULL);
+
+	if(!JS_SetPrivate(cx, obj, filename)) {
+		JS_ReportError(cx, "JS_SetPrivate failed");
+		return JS_FALSE;
+	}
+
+	if(!JS_DefineProperty(cx, obj, "type", JSVAL_VOID, js_archive_type, NULL, JSPROP_ENUMERATE|JSPROP_READONLY)) {
+		JS_ReportError(cx, "JS_DefineProperty failed");
+		return JS_FALSE;
+	}
+
+#ifdef BUILD_JSDOCS
+	js_DescribeSyncObject(cx,obj,"Class used for opening, creating, reading, or writing archive files on the local file system<p>"
+		,31900
+		);
+	js_DescribeSyncConstructor(cx,obj,"To create a new Archive object: <tt>var a = new Archive(<i>filename</i>)</tt>");
+	js_CreateArrayOfStrings(cx, obj, "_property_desc_list", archive_prop_desc, JSPROP_READONLY);
+#endif
+
+	return JS_TRUE;
+}
+
+static void js_finalize_archive(JSContext *cx, JSObject *obj)
+{
+	void* p;
+
+	if((p = JS_GetPrivate(cx, obj)) == NULL)
+		return;
+	free(p);
+	JS_SetPrivate(cx, obj, NULL);
+}
+
+static JSBool js_archive_resolve(JSContext *cx, JSObject *obj, jsid id)
+{
+	char*			name=NULL;
+	JSBool			ret;
+
+	if(id != JSID_VOID && id != JSID_EMPTY) {
+		jsval idval;
+
+		JS_IdToValue(cx, id, &idval);
+		if(JSVAL_IS_STRING(idval))
+			JSSTRING_TO_MSTRING(cx, JSVAL_TO_STRING(idval), name, NULL);
+	}
+
+	ret=js_SyncResolve(cx, obj, name, NULL, js_archive_functions, NULL, 0);
+	if(name)
+		free(name);
+	return ret;
+}
+
+static JSBool js_archive_enumerate(JSContext *cx, JSObject *obj)
+{
+	return js_archive_resolve(cx, obj, JSID_VOID);
+}
+
+JSClass js_archive_class = {
+     "Archive"				/* name			*/
+    ,JSCLASS_HAS_PRIVATE	/* flags		*/
+	,JS_PropertyStub		/* addProperty	*/
+	,JS_PropertyStub		/* delProperty	*/
+	,JS_PropertyStub		/* getProperty	*/
+	,JS_StrictPropertyStub	/* setProperty	*/
+	,js_archive_enumerate	/* enumerate	*/
+	,js_archive_resolve		/* resolve		*/
+	,JS_ConvertStub			/* convert		*/
+	,js_finalize_archive	/* finalize		*/
+};
+
+JSObject* js_CreateArchiveClass(JSContext* cx, JSObject* parent)
+{
+	return JS_InitClass(cx, parent, NULL
+		,&js_archive_class
+		,js_archive_constructor
+		,1		/* number of constructor args */
+		,NULL	/* props, set in constructor */
+		,NULL	/* funcs, set in constructor */
+		,NULL, NULL);
+}
diff --git a/src/sbbs3/js_bbs.cpp b/src/sbbs3/js_bbs.cpp
index df4b0aa5ee22e37a37ca51887e2d6833f44679df..466dacb5af31c481787849a1df737cf6fd01de61 100644
--- a/src/sbbs3/js_bbs.cpp
+++ b/src/sbbs3/js_bbs.cpp
@@ -79,8 +79,6 @@ enum {
 	,BBS_PROP_RLOGIN_TERM
 	,BBS_PROP_CLIENT_NAME
 
-	,BBS_PROP_ALTUL
-
 	,BBS_PROP_ERRORLEVEL		/* READ ONLY */
 
 	/* READ ONLY */
@@ -205,8 +203,6 @@ enum {
 	,"terminal specified during RLogin negotiation"
 	,"client name"
 
-	,"current alternate upload path number"
-
 	,"error level returned from last executed external program"
 
 	/* READ ONLY */
@@ -374,16 +370,16 @@ static JSBool js_bbs_get(JSContext *cx, JSObject *obj, jsid id, jsval *vp)
 			break;
 
 		case BBS_PROP_LOGON_ULB:
-			val=sbbs->logon_ulb;
+			val=(uint32_t)sbbs->logon_ulb;	// TODO: fix for > 4GB!
 			break;
 		case BBS_PROP_LOGON_DLB:
-			val=sbbs->logon_dlb;
+			val=(uint32_t)sbbs->logon_dlb;	// TODO: fix for > 4GB!
 			break;
 		case BBS_PROP_LOGON_ULS:
-			val=sbbs->logon_uls;
+			val=(uint32_t)sbbs->logon_uls;	// TODO: fix for > 4GB!
 			break;
 		case BBS_PROP_LOGON_DLS:
-			val=sbbs->logon_dls;
+			val=(uint32_t)sbbs->logon_dls;	// TODO: fix for > 4GB!
 			break;
 		case BBS_PROP_LOGON_POSTS:
 			val=sbbs->logon_posts;
@@ -455,10 +451,6 @@ static JSBool js_bbs_get(JSContext *cx, JSObject *obj, jsid id, jsval *vp)
 			p=sbbs->client_name;
 			break;
 
-		case BBS_PROP_ALTUL:
-			val=sbbs->altul;
-			break;
-
 		case BBS_PROP_ERRORLEVEL:
 			val=sbbs->errorlevel;
 			break;
@@ -638,7 +630,7 @@ static JSBool js_bbs_get(JSContext *cx, JSObject *obj, jsid id, jsval *vp)
 			break;
 		case BBS_PROP_MSG_OFFSET:
 			if(sbbs->current_msg!=NULL)
-				val=sbbs->current_msg->offset;
+				val=sbbs->current_msg->idx_offset;
 			break;
 		case BBS_PROP_MSG_NUMBER:
 			if(sbbs->current_msg!=NULL)
@@ -705,43 +697,43 @@ static JSBool js_bbs_get(JSContext *cx, JSObject *obj, jsid id, jsval *vp)
 			if(sbbs->current_file==NULL)
 				p=nulstr;
 			else
-				p=sbbs->current_file->uler;
+				p=sbbs->current_file->from;
 			break;
 		case BBS_PROP_FILE_DATE:
 			if(sbbs->current_file==NULL)
 				p=nulstr;
 			else
-				val=sbbs->current_file->date;
+				val=(uint32)sbbs->current_file->time;
 			break;
 		case BBS_PROP_FILE_DATE_ULED:
 			if(sbbs->current_file==NULL)
 				p=nulstr;
 			else
-				val=sbbs->current_file->dateuled;
+				val=sbbs->current_file->hdr.when_imported.time;
 			break;
 		case BBS_PROP_FILE_DATE_DLED:
 			if(sbbs->current_file==NULL)
 				p=nulstr;
 			else
-				val=sbbs->current_file->datedled;
+				val=sbbs->current_file->hdr.last_downloaded;
 			break;
 		case BBS_PROP_FILE_TIMES_DLED:
 			if(sbbs->current_file==NULL)
 				p=nulstr;
 			else
-				val=sbbs->current_file->timesdled;
+				val=sbbs->current_file->hdr.times_downloaded;
 			break;
 		case BBS_PROP_FILE_SIZE:
 			if(sbbs->current_file==NULL)
 				p=nulstr;
-			else
-				val=sbbs->current_file->size;
+			else // TODO: fix for 64-bit file sizes
+				val=(uint32)sbbs->current_file->size;
 			break;
 		case BBS_PROP_FILE_CREDITS:
 			if(sbbs->current_file==NULL)
 				p=nulstr;
 			else
-				val=sbbs->current_file->cdt;
+				val=sbbs->current_file->cost;
 			break;
 		case BBS_PROP_FILE_DIR:
 			if(sbbs->current_file==NULL)
@@ -753,14 +745,14 @@ static JSBool js_bbs_get(JSContext *cx, JSObject *obj, jsid id, jsval *vp)
 			if(sbbs->current_file==NULL)
 				p=nulstr;
 			else
-				val=sbbs->current_file->misc;
+				val=sbbs->current_file->hdr.attr;
 			break;
 
 		case BBS_PROP_BATCH_UPLOAD_TOTAL:
-			val=sbbs->batup_total;
+			val = sbbs->batup_total();
 			break;
 		case BBS_PROP_BATCH_DNLOAD_TOTAL:
-			val=sbbs->batdn_total;
+			val = sbbs->batdn_total();
 			break;
 
 		case BBS_PROP_COMMAND_STR:
@@ -954,11 +946,6 @@ static JSBool js_bbs_set(JSContext *cx, JSObject *obj, jsid id, JSBool strict, j
 				SAFECOPY(sbbs->client_name,p);
 			break;
 
-		case BBS_PROP_ALTUL:
-			if(val<sbbs->cfg.altpaths)
-				sbbs->altul=(ushort)val;
-			break;
-
 		case BBS_PROP_COMMAND_STR:
 			if(p != NULL)
 				sprintf(sbbs->main_csi.str, "%.*s", 1024, p);
@@ -1036,7 +1023,6 @@ static jsSyncPropertySpec js_bbs_properties[] = {
 	{	"rlogin_password"	,BBS_PROP_RLOGIN_PASS	,JSPROP_ENUMERATE	,315},
 	{	"rlogin_terminal"	,BBS_PROP_RLOGIN_TERM	,JSPROP_ENUMERATE	,316},
 	{	"client_name"		,BBS_PROP_CLIENT_NAME	,JSPROP_ENUMERATE	,310},
-	{	"alt_ul_dir"		,BBS_PROP_ALTUL			,JSPROP_ENUMERATE	,310},
 	{	"errorlevel"		,BBS_PROP_ERRORLEVEL	,PROP_READONLY		,312},
 
 	{	"smb_group"			,BBS_PROP_SMB_GROUP			,PROP_READONLY	,310},
@@ -1512,7 +1498,7 @@ js_replace_text(JSContext *cx, uintN argc, jsval *arglist)
 
 	len=strlen(p);
 	if(!len) {
-		sbbs->text[i]=nulstr;
+		sbbs->text[i]=(char*)nulstr;
 		JS_SET_RVAL(cx, arglist, JSVAL_TRUE);
 		free(p);
 	} else {
@@ -1614,7 +1600,7 @@ js_load_text(JSContext *cx, uintN argc, jsval *arglist)
 		}
 		else if(sbbs->text[i][0]==0) {
 			free(sbbs->text[i]);
-			sbbs->text[i]=nulstr;
+			sbbs->text[i]=(char*)nulstr;
 		}
 	}
 	if(i<TOTAL_TEXT)
@@ -2883,34 +2869,6 @@ js_bulkupload(JSContext *cx, uintN argc, jsval *arglist)
 	return(JS_TRUE);
 }
 
-static JSBool
-js_resort_dir(JSContext *cx, uintN argc, jsval *arglist)
-{
-	jsval *argv=JS_ARGV(cx, arglist);
-	uint		dirnum=0;
-	sbbs_t*		sbbs;
-	jsrefcount	rc;
-
-	if((sbbs=js_GetPrivate(cx, JS_THIS_OBJECT(cx, arglist)))==NULL)
-		return(JS_FALSE);
-
-	JS_SET_RVAL(cx, arglist, JSVAL_VOID);
-
-	dirnum=get_dirnum(cx,sbbs,argv[0], argc == 0);
-
-	if(dirnum>=sbbs->cfg.total_dirs) {
-		JS_SET_RVAL(cx, arglist, JSVAL_FALSE);
-		return(JS_TRUE);
-	}
-
-	rc=JS_SUSPENDREQUEST(cx);
-	sbbs->resort(dirnum);
-	JS_RESUMEREQUEST(cx, rc);
-
-	JS_SET_RVAL(cx, arglist, JSVAL_TRUE);
-	return(JS_TRUE);
-}
-
 static JSBool
 js_telnet_gate(JSContext *cx, uintN argc, jsval *arglist)
 {
@@ -3480,7 +3438,6 @@ js_listfiles(JSContext *cx, uintN argc, jsval *arglist)
 	const char	*def=ALLFILES;
 	char*		afspec=NULL;
 	char*		fspec=(char *)def;
-	char		buf[MAX_PATH+1];
 	uint		dirnum;
     JSString*	js_str;
 	sbbs_t*		sbbs;
@@ -3518,9 +3475,6 @@ js_listfiles(JSContext *cx, uintN argc, jsval *arglist)
 	}
 
 	rc=JS_SUSPENDREQUEST(cx);
-	if(!(mode&(FL_FINDDESC|FL_EXFIND)))
-		fspec=padfname(fspec,buf);
-
 	JS_SET_RVAL(cx, arglist, INT_TO_JSVAL(sbbs->listfiles(dirnum,fspec,0 /* tofile */,mode)));
 	if(afspec)
 		free(afspec);
@@ -3536,7 +3490,6 @@ js_listfileinfo(JSContext *cx, uintN argc, jsval *arglist)
 	uint32		mode=FI_INFO;
 	const char	*def=ALLFILES;
 	char*		fspec=(char *)def;
-	char		buf[MAX_PATH+1];
 	uint		dirnum;
     JSString*	js_str;
 	sbbs_t*		sbbs;
@@ -3573,7 +3526,7 @@ js_listfileinfo(JSContext *cx, uintN argc, jsval *arglist)
 	}
 
 	rc=JS_SUSPENDREQUEST(cx);
-	JS_SET_RVAL(cx, arglist, INT_TO_JSVAL(sbbs->listfileinfo(dirnum,padfname(fspec,buf),mode)));
+	JS_SET_RVAL(cx, arglist, INT_TO_JSVAL(sbbs->listfileinfo(dirnum, fspec, mode)));
 	if(fspec != def)
 		free(fspec);
 	JS_RESUMEREQUEST(cx, rc);
@@ -4464,10 +4417,6 @@ static jsSyncMethodSpec js_bbs_functions[] = {
 		"specified by number or internal code")
 	,310
 	},
-	{"resort_dir",		js_resort_dir,		1,	JSTYPE_BOOLEAN,	JSDOCSTR("[directory=<i>current</i>]")
-	,JSDOCSTR("re-sort the file directory specified by number or internal code)")
-	,310
-	},
 	{"list_files",		js_listfiles,		1,	JSTYPE_NUMBER,	JSDOCSTR("[directory=<i>current</i>] [,filespec=<tt>\"*.*\"</tt> or search_string] [,mode=<tt>FL_NONE</tt>]")
 	,JSDOCSTR("list files in the specified file directory, "
 		"optionally specifying a file specification (wildcards) or a description search string, "
diff --git a/src/sbbs3/js_com.c b/src/sbbs3/js_com.c
index c38f94f94d81ad21f95ef7f85684187fd7ee76a4..43fcc93935cb3ac8fe540cb3154d22fe18f66196 100644
--- a/src/sbbs3/js_com.c
+++ b/src/sbbs3/js_com.c
@@ -191,7 +191,7 @@ js_sendfile(JSContext *cx, uintN argc, jsval *arglist)
 {
 	JSObject *obj=JS_THIS_OBJECT(cx, arglist);
 	jsval *argv=JS_ARGV(cx, arglist);
-	long		len;
+	off_t		len;
 	int			file;
 	char*		fname = NULL;
 	private_t*	p;
@@ -225,18 +225,18 @@ js_sendfile(JSContext *cx, uintN argc, jsval *arglist)
 
 	free(fname);
 	len=filelength(file);
-	if((buf=malloc(len))==NULL) {
+	if((buf=malloc((size_t)len))==NULL) {
 		close(file);
 		return(JS_TRUE);
 	}
-	if(read(file,buf,len)!=len) {
+	if(read(file,buf,(uint)len)!=len) {
 		free(buf);
 		close(file);
 		return(JS_TRUE);
 	}
 	close(file);
 
-	if(comWriteBuf(p->com,(uint8_t *)buf,len)==len) {
+	if(comWriteBuf(p->com,(uint8_t *)buf,(size_t)len)==len) {
 		dbprintf(FALSE, p, "sent %u bytes",len);
 		JS_SET_RVAL(cx, arglist, JSVAL_TRUE);
 	} else {
diff --git a/src/sbbs3/js_console.cpp b/src/sbbs3/js_console.cpp
index 4c21a940af6c42b3759144a4733c8bcf4364a105..8e691b08f376f55c1e0072f859663d5b90fb779b 100644
--- a/src/sbbs3/js_console.cpp
+++ b/src/sbbs3/js_console.cpp
@@ -2211,6 +2211,9 @@ js_clear_console_event(JSContext *cx, uintN argc, jsval *arglist, BOOL once)
 	size_t slen;
 	sbbs_t *sbbs;
 
+	if((sbbs=(sbbs_t*)js_GetClassPrivate(cx, JS_THIS_OBJECT(cx, arglist), &js_console_class))==NULL)
+		return(JS_FALSE);
+
 	if (argc != 2) {
 		JS_ReportError(cx, "console.clearOn() and console.clearOnce() require exactly two parameters");
 		return JS_FALSE;
diff --git a/src/sbbs3/js_file.c b/src/sbbs3/js_file.c
index fe05c0474750034eb958133526b0eadd595ca3c9..a52b1f03c2c667bd79876b2720f7034a1e99d6b7 100644
--- a/src/sbbs3/js_file.c
+++ b/src/sbbs3/js_file.c
@@ -410,7 +410,7 @@ js_raw_read(JSContext *cx, uintN argc, jsval *arglist)
 	fd = fileno(p->fp);
 	lseek(fd, pos, SEEK_SET);
 	len = read(fileno(p->fp),buf,len);
-	fseek(p->fp, pos + (len >= 0 ? len : 0), SEEK_SET);
+	fseeko(p->fp, pos + (len >= 0 ? len : 0), SEEK_SET);
 	dbprintf(FALSE, p, "read %u raw bytes",len);
 	if(len<0)
 		len=0;
@@ -2294,6 +2294,8 @@ enum {
 	,FILE_PROP_CRC32
 	,FILE_PROP_MD5_HEX
 	,FILE_PROP_MD5_B64
+	,FILE_PROP_SHA1_HEX
+	,FILE_PROP_SHA1_B64
 	/* ini style */
 	,FILE_INI_KEY_LEN
 	,FILE_INI_KEY_PREFIX
@@ -2441,8 +2443,9 @@ static JSBool js_file_get(JSContext *cx, JSObject *obj, jsid id, jsval *vp)
 	ushort		c16=0;
 	uint32		c32=~0;
 	MD5			md5_ctx;
+	SHA1_CTX	sha1_ctx;
 	BYTE		block[4096];
-	BYTE		digest[MD5_DIGEST_SIZE];
+	BYTE		digest[SHA1_DIGEST_SIZE];
     jsint       tiny;
 	JSString*	js_str=NULL;
 	private_t*	p;
@@ -2561,6 +2564,8 @@ static JSBool js_file_get(JSContext *cx, JSObject *obj, jsid id, jsval *vp)
 			/* fall-through */
 		case FILE_PROP_MD5_HEX:
 		case FILE_PROP_MD5_B64:
+		case FILE_PROP_SHA1_HEX:
+		case FILE_PROP_SHA1_B64:
 			*vp = JSVAL_VOID;
 			if(p->fp==NULL)
 				break;
@@ -2574,6 +2579,10 @@ static JSBool js_file_get(JSContext *cx, JSObject *obj, jsid id, jsval *vp)
 				case FILE_PROP_MD5_B64:
 					MD5_open(&md5_ctx);
 					break;
+				case FILE_PROP_SHA1_HEX:
+				case FILE_PROP_SHA1_B64:
+					SHA1Init(&sha1_ctx);
+					break;
 			}
 
 			/* calculate */
@@ -2597,6 +2606,10 @@ static JSBool js_file_get(JSContext *cx, JSObject *obj, jsid id, jsval *vp)
 					case FILE_PROP_MD5_B64:
 						MD5_digest(&md5_ctx,block,rd);
 						break;
+					case FILE_PROP_SHA1_HEX:
+					case FILE_PROP_SHA1_B64:
+						SHA1Update(&sha1_ctx,block,rd);
+						break;
 					}
 			}
 			JS_RESUMEREQUEST(cx, rc);
@@ -2616,14 +2629,23 @@ static JSBool js_file_get(JSContext *cx, JSObject *obj, jsid id, jsval *vp)
 				case FILE_PROP_MD5_B64:
 					MD5_close(&md5_ctx,digest);
 					if(tiny==FILE_PROP_MD5_HEX)
-						MD5_hex((BYTE*)str,digest);
+						MD5_hex(str,digest);
+					else
+						b64_encode(str,sizeof(str)-1,(char *)digest,sizeof(digest));
+					js_str=JS_NewStringCopyZ(cx, str);
+					break;
+				case FILE_PROP_SHA1_HEX:
+				case FILE_PROP_SHA1_B64:
+					SHA1Final(&sha1_ctx,digest);
+					if(tiny==FILE_PROP_SHA1_HEX)
+						SHA1_hex(str,digest);
 					else
 						b64_encode(str,sizeof(str)-1,(char *)digest,sizeof(digest));
 					js_str=JS_NewStringCopyZ(cx, str);
 					break;
 			}
 			rc=JS_SUSPENDREQUEST(cx);
-			fseek(p->fp,offset,SEEK_SET);	/* restore saved file position */
+			fseeko(p->fp,offset,SEEK_SET);	/* restore saved file position */
 			JS_RESUMEREQUEST(cx, rc);
 			if(js_str!=NULL)
 				*vp = STRING_TO_JSVAL(js_str);
@@ -2696,6 +2718,8 @@ static jsSyncPropertySpec js_file_properties[] = {
 	{	"chksum"			,FILE_PROP_CHKSUM		,FILE_PROP_FLAGS,	311},
 	{	"md5_hex"			,FILE_PROP_MD5_HEX		,FILE_PROP_FLAGS,	311},
 	{	"md5_base64"		,FILE_PROP_MD5_B64		,FILE_PROP_FLAGS,	311},
+	{	"sha1_hex"			,FILE_PROP_SHA1_HEX		,FILE_PROP_FLAGS,	31900},
+	{	"sha1_base64"		,FILE_PROP_SHA1_B64		,FILE_PROP_FLAGS,	31900},
 	/* ini style elements */
 	{	"ini_key_len"				,FILE_INI_KEY_LEN				,JSPROP_ENUMERATE,	317},
 	{	"ini_key_prefix"			,FILE_INI_KEY_PREFIX			,JSPROP_ENUMERATE,	317},
diff --git a/src/sbbs3/js_file_area.c b/src/sbbs3/js_file_area.c
index 30a9f9db78f0d61a1cb4fdaf40a87d594fce719e..962b72feb68e9f37db4f2354f735d3d8fbce96e7 100644
--- a/src/sbbs3/js_file_area.c
+++ b/src/sbbs3/js_file_area.c
@@ -18,6 +18,7 @@
  ****************************************************************************/
 
 #include "sbbs.h"
+#include "filedat.h"
 
 #ifdef JAVASCRIPT
 
@@ -26,7 +27,6 @@
 static char* file_area_prop_desc[] = {
 	 "minimum amount of available disk space (in kilobytes) required for user uploads to be allowed"
 	,"file area settings (bitfield) - see <tt>FM_*</tt> in <tt>sbbsdefs.js</tt> for details"
-	,"array of alternative file paths.  NOTE: this array is zero-based, but alt path fields are one-based."
 	,NULL
 };
 
@@ -52,6 +52,7 @@ static char* dir_prop_desc[] = {
 	,"directory internal code"
 	,"directory name"
 	,"directory description"
+	,"directory area tag for file echoes <i>(introduced in v3.19)</i>"
 	,"directory file storage location"
 	,"directory access requirements"
 	,"directory upload requirements"
@@ -63,13 +64,14 @@ static char* dir_prop_desc[] = {
 	,"directory data storage location"
 	,"toggle options (bitfield)"
 	,"sequential (slow storage) device number"
-	,"sort order (see <tt>SORT_*</tt> in <tt>sbbsdefs.js</tt> for valid values)"
+	,"sort order (see <tt>FileBase.SORT</tt> for valid values)"
 	,"configured maximum number of files"
 	,"configured maximum age (in days) of files before expiration"
 	,"percent of file size awarded uploader in credits upon file upload"
 	,"percent of file size awarded uploader in credits upon subsequent downloads"
 	,"directory link (for HTML index)"
 	,"number of files currently in this directory <i>(introduced in v3.18c)</i>"
+	,"timestamp of file base index of this directory <i>(introduced in v3.19)</i>"
 	,"user has sufficient access to view this directory (e.g. list files) <i>(introduced in v3.18)</i>"
 	,"user has sufficient access to upload files to this directory"
 	,"user has sufficient access to download files from this directory"
@@ -108,6 +110,7 @@ js_file_area_finalize(JSContext *cx, JSObject *obj)
 /***************************************/
 enum {
 	 DIR_PROP_FILES
+	,DIR_PROP_UPDATE_TIME
 	,DIR_PROP_CAN_ACCESS
 	,DIR_PROP_CAN_UPLOAD
 	,DIR_PROP_CAN_DOWNLOAD
@@ -119,6 +122,7 @@ static struct JSPropertySpec js_dir_properties[] = {
 /*		 name				,tinyid		,flags	*/
 
 	{	"files"			,DIR_PROP_FILES			,JSPROP_ENUMERATE|JSPROP_SHARED|JSPROP_READONLY },
+	{	"update_time"	,DIR_PROP_UPDATE_TIME	,JSPROP_ENUMERATE|JSPROP_SHARED|JSPROP_READONLY },
 	{	"can_access"	,DIR_PROP_CAN_ACCESS	,JSPROP_ENUMERATE|JSPROP_SHARED|JSPROP_READONLY },
 	{	"can_upload"	,DIR_PROP_CAN_UPLOAD	,JSPROP_ENUMERATE|JSPROP_SHARED|JSPROP_READONLY },
 	{	"can_download"	,DIR_PROP_CAN_DOWNLOAD	,JSPROP_ENUMERATE|JSPROP_SHARED|JSPROP_READONLY },
@@ -143,6 +147,9 @@ static JSBool js_dir_get(JSContext *cx, JSObject *obj, jsid id, jsval *vp)
 		case DIR_PROP_FILES:
 			*vp = UINT_TO_JSVAL(getfiles(p->cfg, p->dirnum));
 			break;
+		case DIR_PROP_UPDATE_TIME:
+			*vp = UINT_TO_JSVAL((uint32_t)dir_newfiletime(p->cfg, p->dirnum));
+			break;
 		case DIR_PROP_CAN_ACCESS:
 			*vp = BOOLEAN_TO_JSVAL(p->user == NULL || can_user_access_dir(p->cfg, p->dirnum, p->user, p->client));
 			break;
@@ -177,12 +184,12 @@ static JSClass js_dir_class = {
 
 JSBool DLLCALL js_file_area_resolve(JSContext* cx, JSObject* areaobj, jsid id)
 {
+	char		str[128];
 	char		vpath[MAX_PATH+1];
 	JSObject*	alllibs;
 	JSObject*	alldirs;
 	JSObject*	libobj;
 	JSObject*	dirobj;
-	JSObject*	alt_list;
 	JSObject*	lib_list;
 	JSObject*	dir_list;
 	JSString*	js_str;
@@ -223,29 +230,6 @@ JSBool DLLCALL js_file_area_resolve(JSContext* cx, JSObject* areaobj, jsid id)
 			return(JS_TRUE);
 	}
 
-	if(name==NULL || strcmp(name, "alt_paths")==0) {
-		if(name)
-			free(name);
-		/* file_area.alt_paths[] */
-		if((alt_list=JS_NewArrayObject(cx, 0, NULL))==NULL) 
-			return JS_FALSE;
-
-		val=OBJECT_TO_JSVAL(alt_list);
-		if(!JS_SetProperty(cx, areaobj, "alt_paths", &val)) 
-			return JS_FALSE;
-
-		for (l=0; l<p->cfg->altpaths; l++) {
-			if((js_str=JS_NewStringCopyZ(cx, p->cfg->altpath[l]))==NULL)
-				return JS_FALSE;
-			val=STRING_TO_JSVAL(js_str);
-
-			if(!JS_SetElement(cx, alt_list, l, &val))
-				return JS_FALSE;
-		}
-		if(name)
-			return(JS_TRUE);
-	}
-
 #ifdef BUILD_JSDOCS
 	js_CreateArrayOfStrings(cx, areaobj, "_property_desc_list", file_area_prop_desc, JSPROP_READONLY);
 #endif
@@ -445,6 +429,12 @@ JSBool DLLCALL js_file_area_resolve(JSContext* cx, JSObject* areaobj, jsid id)
 				if(!JS_SetProperty(cx, dirobj, "description", &val))
 					return JS_FALSE;
 
+				if((js_str=JS_NewStringCopyZ(cx, dir_area_tag(p->cfg, p->cfg->dir[d], str, sizeof(str))))==NULL)
+					return JS_FALSE;
+				val=STRING_TO_JSVAL(js_str);
+				if(!JS_SetProperty(cx, dirobj, "area_tag", &val))
+					return JS_FALSE;
+
 				if((js_str=JS_NewStringCopyZ(cx, p->cfg->dir[d]->path))==NULL)
 					return JS_FALSE;
 				val=STRING_TO_JSVAL(js_str);
diff --git a/src/sbbs3/js_filebase.c b/src/sbbs3/js_filebase.c
new file mode 100644
index 0000000000000000000000000000000000000000..475a58a95d471e65d622496ba2ff9cc2ccfaf905
--- /dev/null
+++ b/src/sbbs3/js_filebase.c
@@ -0,0 +1,1619 @@
+/* Synchronet JavaScript "FileBase" Object */
+
+/****************************************************************************
+ * @format.tab-size 4		(Plain Text/Source Code File Header)			*
+ * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
+ *																			*
+ * Copyright Rob Swindell - http://www.synchro.net/copyright.html			*
+ *																			*
+ * 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.					*
+ * See the GNU General Public License for more details: gpl.txt or			*
+ * http://www.fsf.org/copyleft/gpl.html										*
+ *																			*
+ * For Synchronet coding style and modification guidelines, see				*
+ * http://www.synchro.net/source.html										*
+ *																			*
+ * Note: If this box doesn't appear square, then you need to fix your tabs.	*
+ ****************************************************************************/
+
+#include "sbbs.h"
+#include "filedat.h"
+#include "js_request.h"
+#include <stdbool.h>
+
+typedef struct
+{
+	smb_t	smb;
+	int		smb_result;
+
+} private_t;
+
+/* Destructor */
+
+static void js_finalize_filebase(JSContext *cx, JSObject *obj)
+{
+	private_t* p;
+
+	if((p=(private_t*)JS_GetPrivate(cx,obj))==NULL)
+		return;
+
+	if(SMB_IS_OPEN(&(p->smb)))
+		smb_close(&(p->smb));
+
+	free(p);
+
+	JS_SetPrivate(cx, obj, NULL);
+}
+
+/* Methods */
+
+extern JSClass js_filebase_class;
+
+static JSBool
+js_open(JSContext *cx, uintN argc, jsval *arglist)
+{
+	JSObject* obj = JS_THIS_OBJECT(cx, arglist);
+	private_t* p;
+	jsrefcount	rc;
+	scfg_t*		scfg;
+
+	scfg = JS_GetRuntimePrivate(JS_GetRuntime(cx));
+	if(scfg == NULL) {
+		JS_ReportError(cx, "JS_GetRuntimePrivate returned NULL");
+		return JS_FALSE;
+	}
+
+	if((p=(private_t*)js_GetClassPrivate(cx, obj, &js_filebase_class))==NULL) {
+		return JS_FALSE;
+	}
+
+	JS_SET_RVAL(cx, arglist, JSVAL_FALSE);
+
+	if(p->smb.dirnum==INVALID_DIR
+		&& strchr(p->smb.file,'/')==NULL
+		&& strchr(p->smb.file,'\\')==NULL) {
+		JS_ReportError(cx,"Unrecognized filebase code: %s",p->smb.file);
+		return JS_TRUE;
+	}
+
+	rc=JS_SUSPENDREQUEST(cx);
+	if((p->smb_result = smb_open_dir(scfg, &(p->smb), p->smb.dirnum)) != SMB_SUCCESS) {
+		JS_RESUMEREQUEST(cx, rc);
+		return JS_TRUE;
+	}
+	JS_RESUMEREQUEST(cx, rc);
+
+	JS_SET_RVAL(cx, arglist, JSVAL_TRUE);
+	return JS_TRUE;
+}
+
+static JSBool
+js_close(JSContext *cx, uintN argc, jsval *arglist)
+{
+	JSObject* obj = JS_THIS_OBJECT(cx, arglist);
+	private_t* p;
+	jsrefcount	rc;
+
+	if((p=(private_t*)js_GetClassPrivate(cx, obj, &js_filebase_class))==NULL) {
+		return JS_FALSE;
+	}
+
+	JS_SET_RVAL(cx, arglist, JSVAL_VOID);
+
+	rc=JS_SUSPENDREQUEST(cx);
+	smb_close(&(p->smb));
+	JS_RESUMEREQUEST(cx, rc);
+
+	return JS_TRUE;
+}
+
+static JSBool
+js_dump_file(JSContext *cx, uintN argc, jsval *arglist)
+{
+	JSObject*	obj = JS_THIS_OBJECT(cx, arglist);
+	jsval*		argv = JS_ARGV(cx, arglist);
+	private_t*	p;
+	char*		filename = NULL;
+
+	JS_SET_RVAL(cx, arglist, JSVAL_NULL);
+
+	if((p=(private_t*)js_GetClassPrivate(cx, obj, &js_filebase_class))==NULL)
+		return JS_FALSE;
+
+	if(!SMB_IS_OPEN(&(p->smb))) {
+		JS_ReportError(cx, "FileBase is not open");
+		return JS_FALSE;
+	}
+
+	uintN argn = 0;
+	if(argn < argc)	{
+		JSVALUE_TO_MSTRING(cx, argv[argn], filename, NULL);
+		HANDLE_PENDING(cx, filename);
+		argn++;
+	}
+	if(filename == NULL)
+		return JS_FALSE;
+
+	file_t file;
+	if((p->smb_result = smb_loadfile(&p->smb, filename, &file, file_detail_normal)) == SMB_SUCCESS) {
+		str_list_t list = smb_msghdr_str_list(&file);
+		if(list != NULL) {
+			JSObject* array;
+			if((array = JS_NewArrayObject(cx, 0, NULL)) == NULL) {
+				free(filename);
+				JS_ReportError(cx, "JS_NewArrayObject failure");
+				return JS_FALSE;
+			}
+			JS_SET_RVAL(cx, arglist, OBJECT_TO_JSVAL(array));
+			for(int i = 0; list[i] != NULL; i++) {
+				JSString* js_str = JS_NewStringCopyZ(cx, list[i]);
+				if(js_str == NULL)
+					break;
+				JS_DefineElement(cx, array, i, STRING_TO_JSVAL(js_str), NULL, NULL, JSPROP_ENUMERATE);
+			}
+			strListFree(&list);
+		}
+	}
+	smb_freefilemem(&file);
+	free(filename);
+	return JS_TRUE;
+}
+
+static bool
+set_file_properties(JSContext *cx, JSObject* obj, file_t* f, enum file_detail detail)
+{
+	jsval		val;
+	JSString*	js_str;
+	const uintN flags = JSPROP_ENUMERATE;
+
+	scfg_t* scfg = JS_GetRuntimePrivate(JS_GetRuntime(cx));
+	if(scfg == NULL) {
+		JS_ReportError(cx, "JS_GetRuntimePrivate returned NULL");
+		return JS_FALSE;
+	}
+	if(f->name == NULL
+		|| (js_str = JS_NewStringCopyZ(cx, f->name)) == NULL
+		|| !JS_DefineProperty(cx, obj, "name", STRING_TO_JSVAL(js_str), NULL, NULL, flags))
+		return false;
+
+	if(f->from != NULL
+		&& ((js_str = JS_NewStringCopyZ(cx, f->from)) == NULL
+			|| !JS_DefineProperty(cx, obj, "from", STRING_TO_JSVAL(js_str), NULL, NULL, flags)))
+		return false;
+
+	val = BOOLEAN_TO_JSVAL(f->idx.attr & FILE_ANONYMOUS);
+	if((val == JSVAL_TRUE || detail > file_detail_extdesc)
+		&& !JS_DefineProperty(cx, obj, "anon", val, NULL, NULL, flags))
+		return false;
+
+	if(f->desc != NULL
+		&& ((js_str = JS_NewStringCopyZ(cx, f->desc)) == NULL
+			|| !JS_DefineProperty(cx, obj, "desc", STRING_TO_JSVAL(js_str), NULL, NULL, flags)))
+		return false;
+
+	if(f->extdesc != NULL && *f->extdesc != '\0'
+		&& ((js_str = JS_NewStringCopyZ(cx, f->extdesc)) == NULL
+			|| !JS_DefineProperty(cx, obj, "extdesc", STRING_TO_JSVAL(js_str), NULL, NULL, flags)))
+		return false;
+
+	if(f->cost > 0 || detail > file_detail_extdesc) {
+		val = UINT_TO_JSVAL(f->cost);
+		if(!JS_DefineProperty(cx, obj, "cost", val, NULL, NULL, flags))
+			return false;
+	}
+	val = UINT_TO_JSVAL(f->idx.size);
+	if(!JS_DefineProperty(cx, obj, "size", val, NULL, NULL, flags))
+		return false;
+
+	val = UINT_TO_JSVAL(f->hdr.when_written.time);
+	if(!JS_DefineProperty(cx, obj, "time", val, NULL, NULL, flags))
+		return false;
+	if(f->hdr.when_imported.time > 0 || detail > file_detail_extdesc) {
+		val = UINT_TO_JSVAL(f->hdr.when_imported.time);
+		if(!JS_DefineProperty(cx, obj, "added", val, NULL, NULL, flags))
+			return false;
+	}
+	if(f->hdr.last_downloaded > 0 || detail > file_detail_extdesc) {
+		val = UINT_TO_JSVAL(f->hdr.last_downloaded);
+		if(!JS_DefineProperty(cx, obj, "last_downloaded", val, NULL, NULL, flags))
+			return false;
+	}
+	if(f->hdr.times_downloaded > 0 || detail > file_detail_extdesc) {
+		val = UINT_TO_JSVAL(f->hdr.times_downloaded);
+		if(!JS_DefineProperty(cx, obj, "times_downloaded", val, NULL, NULL, flags))
+			return false;
+	}
+	if(f->file_idx.hash.flags & SMB_HASH_CRC16) {
+		val = UINT_TO_JSVAL(f->file_idx.hash.data.crc16);
+		if(!JS_DefineProperty(cx, obj, "crc16", val, NULL, NULL, flags))
+			return false;
+	}
+	if(f->file_idx.hash.flags & SMB_HASH_CRC32) {
+		val = UINT_TO_JSVAL(f->file_idx.hash.data.crc32);
+		if(!JS_DefineProperty(cx, obj, "crc32", val, NULL, NULL, flags))
+			return false;
+	}
+	if(f->file_idx.hash.flags & SMB_HASH_MD5) {
+		char hex[128];
+		if((js_str = JS_NewStringCopyZ(cx, MD5_hex(hex, f->file_idx.hash.data.md5))) == NULL
+			|| !JS_DefineProperty(cx, obj, "md5", STRING_TO_JSVAL(js_str), NULL, NULL, flags))
+			return false;
+	}
+	if(f->file_idx.hash.flags & SMB_HASH_SHA1) {
+		char hex[128];
+		if((js_str = JS_NewStringCopyZ(cx, SHA1_hex(hex, f->file_idx.hash.data.sha1))) == NULL
+			|| !JS_DefineProperty(cx, obj, "sha1", STRING_TO_JSVAL(js_str), NULL, NULL, flags))
+			return false;
+	}
+	if(f->tags != NULL
+		&& ((js_str = JS_NewStringCopyZ(cx, f->tags)) == NULL
+			|| !JS_DefineProperty(cx, obj, "tags", STRING_TO_JSVAL(js_str), NULL, NULL, flags)))
+		return false;
+
+	return true;
+}
+
+static BOOL
+parse_file_index_properties(JSContext *cx, JSObject* obj, fileidxrec_t* idx)
+{
+	char*		cp = NULL;
+	size_t		cp_sz = 0;
+	jsval		val;
+	const char* prop_name;
+
+	if(JS_GetProperty(cx, obj, prop_name = "name", &val) && !JSVAL_NULL_OR_VOID(val)) {
+		JSVALUE_TO_RASTRING(cx, val, cp, &cp_sz, NULL);
+		HANDLE_PENDING(cx, cp);
+		if(cp==NULL) {
+			JS_ReportError(cx, "Invalid '%s' string in file object", prop_name);
+			return FALSE;
+		}
+		SAFECOPY(idx->name, cp);
+	}
+	if(JS_GetProperty(cx, obj, prop_name = "size", &val) && !JSVAL_NULL_OR_VOID(val)) {
+		if(!JS_ValueToECMAUint32(cx, val, &idx->idx.size)) {
+			JS_ReportError(cx, "Error converting adding '%s' property to Uint32", prop_name);
+			return FALSE;
+		}
+	}
+	if(JS_GetProperty(cx, obj, prop_name = "crc16", &val) && !JSVAL_NULL_OR_VOID(val)) {
+		idx->hash.data.crc16 = JSVAL_TO_INT(val);
+		idx->hash.flags |= SMB_HASH_CRC16;
+	}
+	if(JS_GetProperty(cx, obj, prop_name = "crc32", &val) && !JSVAL_NULL_OR_VOID(val)) {
+		if(!JS_ValueToECMAUint32(cx, val, &idx->hash.data.crc32)) {
+			JS_ReportError(cx, "Error converting adding '%s' property to Uint32", prop_name);
+			return FALSE;
+		}
+		idx->hash.flags |= SMB_HASH_CRC32;
+	}
+	if(JS_GetProperty(cx, obj, prop_name = "md5", &val) && !JSVAL_NULL_OR_VOID(val)) {
+		JSVALUE_TO_RASTRING(cx, val, cp, &cp_sz, NULL);
+		HANDLE_PENDING(cx, cp);
+		if(cp==NULL || strlen(cp) != MD5_DIGEST_SIZE * 2) {
+			free(cp);
+			JS_ReportError(cx, "Invalid '%s' string in file object", prop_name);
+			return FALSE;
+		}
+		for(int i = 0; i < MD5_DIGEST_SIZE * 2; i += 2) {
+			idx->hash.data.md5[i/2] = HEX_CHAR_TO_INT(*(cp + i)) * 16;
+			idx->hash.data.md5[i/2] += HEX_CHAR_TO_INT(*(cp + i + 1));
+		}
+		idx->hash.flags |= SMB_HASH_MD5;
+	}
+	if(JS_GetProperty(cx, obj, prop_name = "sha1", &val) && !JSVAL_NULL_OR_VOID(val)) {
+		JSVALUE_TO_RASTRING(cx, val, cp, &cp_sz, NULL);
+		HANDLE_PENDING(cx, cp);
+		if(cp==NULL || strlen(cp) != SHA1_DIGEST_SIZE * 2) {
+			free(cp);
+			JS_ReportError(cx, "Invalid '%s' string in file object", prop_name);
+			return FALSE;
+		}
+		for(int i = 0; i < SHA1_DIGEST_SIZE * 2; i += 2) {
+			idx->hash.data.sha1[i/2] = HEX_CHAR_TO_INT(*(cp + i)) * 16;
+			idx->hash.data.sha1[i/2] += HEX_CHAR_TO_INT(*(cp + i + 1));
+		}
+		idx->hash.flags |= SMB_HASH_SHA1;
+	}
+	free(cp);
+	return TRUE;
+}
+
+static int
+parse_file_properties(JSContext *cx, JSObject* obj, file_t* file, char** extdesc)
+{
+	char*		cp = NULL;
+	size_t		cp_sz = 0;
+	jsval		val;
+	int result = SMB_ERR_NOT_FOUND;
+
+	const char* prop_name = "name";
+	if(JS_GetProperty(cx, obj, prop_name, &val) && !JSVAL_NULL_OR_VOID(val)) {
+		JSVALUE_TO_RASTRING(cx, val, cp, &cp_sz, NULL);
+		HANDLE_PENDING(cx, cp);
+		if(cp==NULL) {
+			JS_ReportError(cx, "Invalid '%s' string in file object", prop_name);
+			return SMB_FAILURE;
+		}
+		if((result = smb_new_hfield_str(file, SMB_FILENAME, cp)) != SMB_SUCCESS) {
+			free(cp);
+			JS_ReportError(cx, "Error %d adding '%s' property to file object", result, prop_name);
+			return result;
+		}
+	}
+
+	prop_name = "from";
+	if(JS_GetProperty(cx, obj, prop_name, &val) && !JSVAL_NULL_OR_VOID(val)) {
+		JSVALUE_TO_RASTRING(cx, val, cp, &cp_sz, NULL);
+		HANDLE_PENDING(cx, cp);
+		if(cp==NULL) {
+			JS_ReportError(cx, "Invalid '%s' string in file object", prop_name);
+			return SMB_FAILURE;
+		}
+		if((result = smb_new_hfield_str(file, SMB_FILEUPLOADER, cp)) != SMB_SUCCESS) {
+			free(cp);
+			JS_ReportError(cx, "Error %d adding '%s' property to file object", result, prop_name);
+			return result;
+		}
+	}
+
+	prop_name = "desc";
+	if(JS_GetProperty(cx, obj, prop_name, &val) && !JSVAL_NULL_OR_VOID(val)) {
+		JSVALUE_TO_RASTRING(cx, val, cp, &cp_sz, NULL);
+		HANDLE_PENDING(cx, cp);
+		if(cp==NULL) {
+			JS_ReportError(cx, "Invalid '%s' string in file object", prop_name);
+			return SMB_FAILURE;
+		}
+		if((result = smb_new_hfield_str(file, SMB_FILEDESC, cp)) != SMB_SUCCESS) {
+			free(cp);
+			JS_ReportError(cx, "Error %d adding '%s' property to file object", result, prop_name);
+			return result;
+		}
+	}
+	prop_name = "extdesc";
+	if(extdesc != NULL && JS_GetProperty(cx, obj, prop_name, &val) && !JSVAL_NULL_OR_VOID(val)) {
+		FREE_AND_NULL(*extdesc);
+		JSVALUE_TO_MSTRING(cx, val, *extdesc, NULL);
+		HANDLE_PENDING(cx, *extdesc);
+		if(*extdesc == NULL) {
+			JS_ReportError(cx, "Invalid '%s' string in file object", prop_name);
+			return SMB_ERR_MEM;
+		}
+		truncsp(*extdesc);
+	}
+	prop_name = "tags";
+	if(JS_GetProperty(cx, obj, prop_name, &val) && !JSVAL_NULL_OR_VOID(val)) {
+		JSVALUE_TO_RASTRING(cx, val, cp, &cp_sz, NULL);
+		HANDLE_PENDING(cx, cp);
+		if(cp==NULL) {
+			JS_ReportError(cx, "Invalid '%s' string in file object", prop_name);
+			return SMB_FAILURE;
+		}
+		if((result = smb_new_hfield_str(file, SMB_TAGS, cp)) != SMB_SUCCESS) {
+			free(cp);
+			JS_ReportError(cx, "Error %d adding '%s' property to file object", result, prop_name);
+			return result;
+		}
+	}
+	prop_name = "cost";
+	if(JS_GetProperty(cx, obj, prop_name, &val) && !JSVAL_NULL_OR_VOID(val)) {
+		uint32_t cost = 0;
+		if(!JS_ValueToECMAUint32(cx, val, &cost)) {
+			JS_ReportError(cx, "Error converting adding '%s' property to Uint32", prop_name);
+			return SMB_FAILURE;
+		}
+		if((result = smb_new_hfield(file, SMB_COST, sizeof(cost), &cost)) != SMB_SUCCESS) {
+			free(cp);
+			JS_ReportError(cx, "Error %d adding '%s' property to file object", result, prop_name);
+			return result;
+		}
+	}
+
+	if(JS_GetProperty(cx, obj, "anon", &val) && val == JSVAL_TRUE)
+		file->hdr.attr |= FILE_ANONYMOUS;
+
+	if(!parse_file_index_properties(cx, obj, &file->file_idx))
+		result = SMB_FAILURE;
+
+	free(cp);
+	return result;
+}
+
+static JSBool
+js_hash_file(JSContext *cx, uintN argc, jsval *arglist)
+{
+	JSObject*	obj = JS_THIS_OBJECT(cx, arglist);
+	jsval*		argv = JS_ARGV(cx, arglist);
+	private_t*	p;
+	char		path[MAX_PATH + 1];
+	char*		filename = NULL;
+	enum file_detail detail = file_detail_normal;
+	jsrefcount	rc;
+
+	JS_SET_RVAL(cx, arglist, JSVAL_NULL);
+
+	scfg_t* scfg = JS_GetRuntimePrivate(JS_GetRuntime(cx));
+	if(scfg == NULL) {
+		JS_ReportError(cx, "JS_GetRuntimePrivate returned NULL");
+		return JS_FALSE;
+	}
+
+	if((p=(private_t*)js_GetClassPrivate(cx, obj, &js_filebase_class))==NULL)
+		return JS_FALSE;
+
+	file_t file;
+	ZERO_VAR(file);
+
+	uintN argn = 0;
+	if(argn < argc && JSVAL_IS_STRING(argv[argn]))	{
+		JSVALUE_TO_MSTRING(cx, argv[argn], filename, NULL);
+		HANDLE_PENDING(cx, filename);
+		argn++;
+	}
+	if(filename == NULL) {
+		JS_ReportError(cx, "No filename argument");
+		return JS_TRUE;
+	}
+	rc=JS_SUSPENDREQUEST(cx);
+	if(getfname(filename) != filename)
+		SAFECOPY(path, filename);
+	else {
+		file.name = filename;
+		// read index record, if it exists (for altpath)
+		smb_findfile(&p->smb, filename, &file);
+		getfilepath(scfg, &file, path);
+	}
+	off_t size = flength(path);
+	if(size == -1)
+		JS_ReportError(cx, "File does not exist: %s", path);
+	else {
+		file.idx.size = (uint32_t)size;
+		if((p->smb_result = smb_hashfile(path, size, &file.file_idx.hash.data)) > 0) {
+			file.file_idx.hash.flags = p->smb_result;
+			file.hdr.when_written.time = (uint32_t)fdate(path);
+			JSObject* fobj;
+			if((fobj = JS_NewObject(cx, NULL, NULL, obj)) == NULL)
+				JS_ReportError(cx, "object allocation failure, line %d", __LINE__);
+			else {
+				set_file_properties(cx, fobj, &file, detail);
+				JS_SET_RVAL(cx, arglist, OBJECT_TO_JSVAL(fobj));
+			}
+		}
+	}
+	JS_RESUMEREQUEST(cx, rc);
+	free(filename);
+	smb_freefilemem(&file);
+
+	return JS_TRUE;
+}
+
+static JSBool
+js_get_file(JSContext *cx, uintN argc, jsval *arglist)
+{
+	JSObject*	obj = JS_THIS_OBJECT(cx, arglist);
+	jsval*		argv = JS_ARGV(cx, arglist);
+	private_t*	p;
+	char*		filename = NULL;
+	enum file_detail detail = file_detail_normal;
+	jsrefcount	rc;
+
+	JS_SET_RVAL(cx, arglist, JSVAL_NULL);
+
+	scfg_t* scfg = JS_GetRuntimePrivate(JS_GetRuntime(cx));
+	if(scfg == NULL) {
+		JS_ReportError(cx, "JS_GetRuntimePrivate returned NULL");
+		return JS_FALSE;
+	}
+
+	if((p=(private_t*)js_GetClassPrivate(cx, obj, &js_filebase_class))==NULL)
+		return JS_FALSE;
+
+	if(!SMB_IS_OPEN(&(p->smb))) {
+		JS_ReportError(cx, "FileBase is not open");
+		return JS_FALSE;
+	}
+
+	file_t file;
+	ZERO_VAR(file);
+
+	uintN argn = 0;
+	if(argn < argc && JSVAL_IS_STRING(argv[argn]))	{
+		JSVALUE_TO_MSTRING(cx, argv[argn], filename, NULL);
+		HANDLE_PENDING(cx, filename);
+		if(filename == NULL)
+			return JS_FALSE;
+		argn++;
+	}
+	if(argn < argc && JSVAL_IS_OBJECT(argv[argn])) {
+		if(!parse_file_index_properties(cx, JSVAL_TO_OBJECT(argv[argn]), &file.file_idx))
+			return JS_TRUE;
+		free(filename);
+		filename = strdup(file.file_idx.name);
+		argn++;
+	}
+	else if(filename == NULL)
+		return JS_TRUE;
+	if(argn < argc && JSVAL_IS_NUMBER(argv[argn])) {
+		detail = JSVAL_TO_INT(argv[argn]);
+		argn++;
+	}
+	rc=JS_SUSPENDREQUEST(cx);
+	if((p->smb_result = smb_findfile(&p->smb, filename, &file)) == SMB_SUCCESS
+		&& (p->smb_result = smb_getfile(&p->smb, &file, detail)) == SMB_SUCCESS) {
+		JSObject* fobj;
+		if((fobj = JS_NewObject(cx, NULL, NULL, obj)) == NULL)
+			JS_ReportError(cx, "object allocation failure, line %d", __LINE__);
+		else {
+			set_file_properties(cx, fobj, &file, detail);
+		    JS_SET_RVAL(cx, arglist, OBJECT_TO_JSVAL(fobj));
+		}
+	}
+	JS_RESUMEREQUEST(cx, rc);
+	free(filename);
+	smb_freefilemem(&file);
+
+	return JS_TRUE;
+}
+
+static JSBool
+js_get_file_list(JSContext *cx, uintN argc, jsval *arglist)
+{
+	JSObject*	obj = JS_THIS_OBJECT(cx, arglist);
+	jsval*		argv = JS_ARGV(cx, arglist);
+	private_t*	p;
+	time_t		t = 0;
+	char*		filespec = NULL;
+	enum file_detail detail = file_detail_normal;
+	enum file_sort sort = FILE_SORT_NAME_A;
+	jsrefcount	rc;
+
+	JS_SET_RVAL(cx, arglist, JSVAL_FALSE);
+
+	scfg_t* scfg = JS_GetRuntimePrivate(JS_GetRuntime(cx));
+	if(scfg == NULL) {
+		JS_ReportError(cx, "JS_GetRuntimePrivate returned NULL");
+		return JS_FALSE;
+	}
+
+	if((p=(private_t*)js_GetClassPrivate(cx, obj, &js_filebase_class))==NULL)
+		return JS_FALSE;
+
+	if(!SMB_IS_OPEN(&(p->smb))) {
+		JS_ReportError(cx, "FileBase is not open");
+		return JS_FALSE;
+	}
+
+	if(p->smb.dirnum != INVALID_DIR)
+		sort = scfg->dir[p->smb.dirnum]->sort;
+
+	uintN argn = 0;
+	if(argn < argc && JSVAL_IS_STRING(argv[argn]))	{
+		JSVALUE_TO_MSTRING(cx, argv[argn], filespec, NULL);
+		HANDLE_PENDING(cx, filespec);
+		if(filespec == NULL)
+			return JS_FALSE;
+		argn++;
+	}
+	if(argn < argc && JSVAL_IS_NUMBER(argv[argn])) {
+		detail = JSVAL_TO_INT(argv[argn]);
+		argn++;
+	}
+	if(argn < argc && JSVAL_IS_NUMBER(argv[argn]))	{
+		t = JSVAL_TO_INT(argv[argn]);
+		argn++;
+	}
+	if(argn < argc && JSVAL_IS_BOOLEAN(argv[argn])) {
+		if(argv[argn++] == JSVAL_FALSE)
+			sort = FILE_SORT_NATURAL;
+		else if(argn < argc && JSVAL_IS_NUMBER(argv[argn])) {
+			sort = JSVAL_TO_INT(argv[argn]);
+			argn++;
+		}
+	}
+
+	rc=JS_SUSPENDREQUEST(cx);
+	JSObject* array;
+    if((array = JS_NewArrayObject(cx, 0, NULL)) == NULL) {
+		free(filespec);
+		JS_RESUMEREQUEST(cx, rc);
+		JS_ReportError(cx, "JS_NewArrayObject failure");
+		return JS_FALSE;
+	}
+
+	size_t file_count;
+	file_t* file_list = loadfiles(&p->smb, filespec, t, detail, sort, &file_count);
+	if(file_list != NULL) {
+		for(size_t i = 0; i < file_count; i++) {
+			JSObject* fobj;
+			if((fobj = JS_NewObject(cx, NULL, NULL, array)) == NULL) {
+				JS_ReportError(cx, "object allocation failure, line %d", __LINE__);
+				break;
+			}
+			set_file_properties(cx, fobj, &file_list[i], detail);
+			JS_DefineElement(cx, array, i, OBJECT_TO_JSVAL(fobj), NULL, NULL, JSPROP_ENUMERATE);
+		}
+		freefiles(file_list, file_count);
+	}
+    JS_SET_RVAL(cx, arglist, OBJECT_TO_JSVAL(array));
+	JS_RESUMEREQUEST(cx, rc);
+	free(filespec);
+
+	return JS_TRUE;
+}
+
+static JSBool
+js_get_file_names(JSContext *cx, uintN argc, jsval *arglist)
+{
+	JSObject*	obj = JS_THIS_OBJECT(cx, arglist);
+	jsval*		argv = JS_ARGV(cx, arglist);
+	private_t*	p;
+	time_t		t = 0;
+	char*		filespec = NULL;
+	enum file_sort sort = FILE_SORT_NAME_A;
+	jsrefcount	rc;
+
+	JS_SET_RVAL(cx, arglist, JSVAL_FALSE);
+
+	scfg_t* scfg = JS_GetRuntimePrivate(JS_GetRuntime(cx));
+	if(scfg == NULL) {
+		JS_ReportError(cx, "JS_GetRuntimePrivate returned NULL");
+		return JS_FALSE;
+	}
+
+	if((p=(private_t*)js_GetClassPrivate(cx, obj, &js_filebase_class))==NULL)
+		return JS_FALSE;
+
+	if(!SMB_IS_OPEN(&(p->smb))) {
+		JS_ReportError(cx, "FileBase is not open");
+		return JS_FALSE;
+	}
+
+	if(p->smb.dirnum != INVALID_DIR)
+		sort = scfg->dir[p->smb.dirnum]->sort;
+
+	uintN argn = 0;
+	if(argn < argc && JSVAL_IS_STRING(argv[argn]))	{
+		JSVALUE_TO_MSTRING(cx, argv[argn], filespec, NULL);
+		HANDLE_PENDING(cx, filespec);
+		if(filespec == NULL)
+			return JS_FALSE;
+		argn++;
+	}
+	if(argn < argc && JSVAL_IS_NUMBER(argv[argn]))	{
+		t = JSVAL_TO_INT(argv[argn]);
+		argn++;
+	}
+	if(argn < argc && JSVAL_IS_BOOLEAN(argv[argn])) {
+		if(argv[argn++] == JSVAL_FALSE)
+			sort = FILE_SORT_NATURAL;
+		else if(argn < argc && JSVAL_IS_NUMBER(argv[argn])) {
+			sort = JSVAL_TO_INT(argv[argn]);
+			argn++;
+		}
+	}
+
+	rc=JS_SUSPENDREQUEST(cx);
+	JSObject* array;
+    if((array = JS_NewArrayObject(cx, 0, NULL)) == NULL) {
+		free(filespec);
+		JS_RESUMEREQUEST(cx, rc);
+		JS_ReportError(cx, "JS_NewArrayObject failure");
+		return JS_FALSE;
+	}
+
+	str_list_t file_list = loadfilenames(&p->smb, filespec, t, sort, NULL);
+	if(file_list != NULL) {
+		for(size_t i = 0; file_list[i] != NULL; i++) {
+			JSString* js_str;
+			if((js_str = JS_NewStringCopyZ(cx, file_list[i]))==NULL)
+				return JS_FALSE;
+			JS_DefineElement(cx, array, i, STRING_TO_JSVAL(js_str), NULL, NULL, JSPROP_ENUMERATE);
+		}
+		strListFree(&file_list);
+	}
+    JS_SET_RVAL(cx, arglist, OBJECT_TO_JSVAL(array));
+	JS_RESUMEREQUEST(cx, rc);
+	free(filespec);
+
+	return JS_TRUE;
+}
+
+static JSBool
+js_get_file_name(JSContext *cx, uintN argc, jsval *arglist)
+{
+	jsval*		argv = JS_ARGV(cx, arglist);
+	char*		filepath = NULL;
+	char		filename[SMB_FILEIDX_NAMELEN + 1] = "";
+
+	JS_SET_RVAL(cx, arglist, JSVAL_NULL);
+
+ 	if(!js_argc(cx, argc, 1))
+		return JS_FALSE;
+
+	uintN argn = 0;
+	JSVALUE_TO_MSTRING(cx, argv[argn], filepath, NULL);
+	HANDLE_PENDING(cx, filepath);
+	JSString* js_str;
+	if((js_str = JS_NewStringCopyZ(cx, smb_fileidxname(getfname(filepath), filename, sizeof(filename)))) != NULL)
+		JS_SET_RVAL(cx, arglist, STRING_TO_JSVAL(js_str));
+
+	return JS_TRUE;
+}
+
+static JSBool
+js_get_file_path(JSContext *cx, uintN argc, jsval *arglist)
+{
+	JSObject*	obj = JS_THIS_OBJECT(cx, arglist);
+	jsval*		argv = JS_ARGV(cx, arglist);
+	private_t*	p;
+	char*		filename = NULL;
+	jsrefcount	rc;
+	file_t file;
+
+	ZERO_VAR(file);
+	JS_SET_RVAL(cx, arglist, JSVAL_NULL);
+
+	scfg_t* scfg = JS_GetRuntimePrivate(JS_GetRuntime(cx));
+	if(scfg == NULL) {
+		JS_ReportError(cx, "JS_GetRuntimePrivate returned NULL");
+		return JS_FALSE;
+	}
+
+	if((p=(private_t*)js_GetClassPrivate(cx, obj, &js_filebase_class))==NULL)
+		return JS_FALSE;
+
+	uintN argn = 0;
+	if(argn < argc && JSVAL_IS_STRING(argv[argn]))	{
+		JSVALUE_TO_MSTRING(cx, argv[argn], filename, NULL);
+		HANDLE_PENDING(cx, filename);
+		argn++;
+	}
+	if(argn < argc && JSVAL_IS_OBJECT(argv[argn])) {
+		if(!parse_file_index_properties(cx, JSVAL_TO_OBJECT(argv[argn]), &file.file_idx))
+			return JS_TRUE;
+		free(filename);
+		filename = strdup(file.file_idx.name);
+		argn++;
+	}
+	else if(filename == NULL)
+		return JS_TRUE;
+
+	rc=JS_SUSPENDREQUEST(cx);
+	if((p->smb_result = smb_loadfile(&p->smb, filename, &file, file_detail_index)) == SMB_SUCCESS) {
+		char path[MAX_PATH + 1];
+		JSString* js_str;
+		if((js_str = JS_NewStringCopyZ(cx, getfilepath(scfg, &file, path))) != NULL)
+			JS_SET_RVAL(cx, arglist, STRING_TO_JSVAL(js_str));
+		smb_freefilemem(&file);
+	}
+	JS_RESUMEREQUEST(cx, rc);
+	free(filename);
+
+	return JS_TRUE;
+}
+
+static JSBool
+js_get_file_size(JSContext *cx, uintN argc, jsval *arglist)
+{
+	JSObject*	obj = JS_THIS_OBJECT(cx, arglist);
+	jsval*		argv = JS_ARGV(cx, arglist);
+	private_t*	p;
+	char*		filename = NULL;
+	jsrefcount	rc;
+	file_t	file;
+
+	ZERO_VAR(file);
+	JS_SET_RVAL(cx, arglist, INT_TO_JSVAL(-1));
+
+	scfg_t* scfg = JS_GetRuntimePrivate(JS_GetRuntime(cx));
+	if(scfg == NULL) {
+		JS_ReportError(cx, "JS_GetRuntimePrivate returned NULL");
+		return JS_FALSE;
+	}
+
+	if((p=(private_t*)js_GetClassPrivate(cx, obj, &js_filebase_class))==NULL)
+		return JS_FALSE;
+
+	uintN argn = 0;
+	if(argn < argc && JSVAL_IS_STRING(argv[argn]))	{
+		JSVALUE_TO_MSTRING(cx, argv[argn], filename, NULL);
+		HANDLE_PENDING(cx, filename);
+		argn++;
+	}
+	if(argn < argc && JSVAL_IS_OBJECT(argv[argn])) {
+		if(!parse_file_index_properties(cx, JSVAL_TO_OBJECT(argv[argn]), &file.file_idx))
+			return JS_TRUE;
+		free(filename);
+		filename = strdup(file.file_idx.name);
+		argn++;
+	}
+	else if(filename == NULL)
+		return JS_TRUE;
+
+	rc=JS_SUSPENDREQUEST(cx);
+	if((p->smb_result = smb_loadfile(&p->smb, filename, &file, file_detail_index)) == SMB_SUCCESS) {
+		char path[MAX_PATH + 1];
+		getfilepath(scfg, &file, path);
+	    JS_SET_RVAL(cx, arglist, DOUBLE_TO_JSVAL((jsdouble)getfilesize(scfg, &file)));
+		smb_freefilemem(&file);
+	}
+	JS_RESUMEREQUEST(cx, rc);
+	free(filename);
+
+	return JS_TRUE;
+}
+
+static JSBool
+js_get_file_time(JSContext *cx, uintN argc, jsval *arglist)
+{
+	JSObject*	obj = JS_THIS_OBJECT(cx, arglist);
+	jsval*		argv = JS_ARGV(cx, arglist);
+	private_t*	p;
+	char*		filename = NULL;
+	jsrefcount	rc;
+	file_t file;
+
+	ZERO_VAR(file);
+	JS_SET_RVAL(cx, arglist, INT_TO_JSVAL(-1));
+
+	scfg_t* scfg = JS_GetRuntimePrivate(JS_GetRuntime(cx));
+	if(scfg == NULL) {
+		JS_ReportError(cx, "JS_GetRuntimePrivate returned NULL");
+		return JS_FALSE;
+	}
+
+	if((p=(private_t*)js_GetClassPrivate(cx, obj, &js_filebase_class))==NULL)
+		return JS_FALSE;
+
+	uintN argn = 0;
+	if(argn < argc && JSVAL_IS_STRING(argv[argn]))	{
+		JSVALUE_TO_MSTRING(cx, argv[argn], filename, NULL);
+		HANDLE_PENDING(cx, filename);
+		argn++;
+	}
+	if(argn < argc && JSVAL_IS_OBJECT(argv[argn])) {
+		if(!parse_file_index_properties(cx, JSVAL_TO_OBJECT(argv[argn]), &file.file_idx))
+			return JS_TRUE;
+		free(filename);
+		filename = strdup(file.file_idx.name);
+		argn++;
+	}
+	else if(filename == NULL)
+		return JS_TRUE;
+
+	rc=JS_SUSPENDREQUEST(cx);
+	if((p->smb_result = smb_loadfile(&p->smb, filename, &file, file_detail_index)) == SMB_SUCCESS) {
+		char path[MAX_PATH + 1];
+		getfilepath(scfg, &file, path);
+	    JS_SET_RVAL(cx, arglist, UINT_TO_JSVAL((uint32)getfiletime(scfg, &file)));
+		smb_freefilemem(&file);
+	}
+	JS_RESUMEREQUEST(cx, rc);
+	free(filename);
+
+	return JS_TRUE;
+}
+
+static void get_diz(scfg_t* scfg, file_t* file, char** extdesc)
+{
+	char diz_fpath[MAX_PATH + 1];
+	if(extract_diz(scfg, file, /* diz_fnames: */NULL, diz_fpath, sizeof(diz_fpath))) {
+		char extbuf[LEN_EXTDESC + 1] = "";
+		str_list_t lines = read_diz(diz_fpath, /* max_line_len: */80);
+		if(lines != NULL) {
+			format_diz(lines, extbuf, sizeof(extbuf), /* allow_ansi: */false);
+			strListFree(&lines);
+			free(*extdesc);
+			*extdesc = strdup(extbuf);
+			if(file->desc == NULL)
+				smb_new_hfield_str(file, SMB_FILEDESC, prep_file_desc(extbuf, extbuf));
+		}
+		(void)remove(diz_fpath);
+	}
+}
+
+static JSBool
+js_add_file(JSContext *cx, uintN argc, jsval *arglist)
+{
+	JSObject*	obj = JS_THIS_OBJECT(cx, arglist);
+	jsval*		argv = JS_ARGV(cx, arglist);
+	private_t*	p;
+	char*		extdesc = NULL;
+	file_t	file;
+	bool		use_diz_always = false;
+	jsrefcount	rc;
+
+	ZERO_VAR(file);
+	JS_SET_RVAL(cx, arglist, JSVAL_FALSE);
+
+	scfg_t* scfg = JS_GetRuntimePrivate(JS_GetRuntime(cx));
+	if(scfg == NULL) {
+		JS_ReportError(cx, "JS_GetRuntimePrivate returned NULL");
+		return JS_FALSE;
+	}
+
+	if((p=(private_t*)js_GetClassPrivate(cx, obj, &js_filebase_class))==NULL)
+		return JS_FALSE;
+
+	if(!SMB_IS_OPEN(&(p->smb))) {
+		JS_ReportError(cx, "FileBase is not open");
+		return JS_FALSE;
+	}
+
+	uintN argn = 0;
+	if(argn < argc && JSVAL_IS_OBJECT(argv[argn])) {
+		p->smb_result = parse_file_properties(cx, JSVAL_TO_OBJECT(argv[argn]), &file, &extdesc);
+		if(p->smb_result != SMB_SUCCESS)
+			return JS_TRUE;
+		argn++;
+	}
+	if(argn < argc && JSVAL_IS_BOOLEAN(argv[argn])) {
+		use_diz_always = JSVAL_TO_BOOLEAN(argv[argn]);
+		argn++;
+	}
+
+	file.dir = p->smb.dirnum;
+	rc=JS_SUSPENDREQUEST(cx);
+	if(file.name != NULL) {
+		if((extdesc == NULL	|| use_diz_always == true)
+			&& file.dir < scfg->total_dirs
+			&& (scfg->dir[file.dir]->misc & DIR_DIZ)) {
+			get_diz(scfg, &file, &extdesc);
+		}
+		char fpath[MAX_PATH + 1];
+		getfilepath(scfg, &file, fpath);
+		p->smb_result = smb_addfile(&p->smb, &file, SMB_SELFPACK, extdesc, fpath);
+		JS_SET_RVAL(cx, arglist, BOOLEAN_TO_JSVAL(p->smb_result == SMB_SUCCESS));
+	}
+	JS_RESUMEREQUEST(cx, rc);
+	smb_freefilemem(&file);
+	free(extdesc);
+
+	return JS_TRUE;
+}
+
+static JSBool
+js_update_file(JSContext *cx, uintN argc, jsval *arglist)
+{
+	JSObject*	obj = JS_THIS_OBJECT(cx, arglist);
+	jsval*		argv = JS_ARGV(cx, arglist);
+	private_t*	p;
+	file_t	file;
+	char*		filename = NULL;
+	JSObject*	fileobj = NULL;
+	bool		use_diz_always = false;
+	jsrefcount	rc;
+
+	ZERO_VAR(file);
+	JS_SET_RVAL(cx, arglist, JSVAL_FALSE);
+
+	scfg_t* scfg = JS_GetRuntimePrivate(JS_GetRuntime(cx));
+	if(scfg == NULL) {
+		JS_ReportError(cx, "JS_GetRuntimePrivate returned NULL");
+		return JS_FALSE;
+	}
+
+	if((p=(private_t*)js_GetClassPrivate(cx, obj, &js_filebase_class))==NULL)
+		return JS_FALSE;
+
+	if(!SMB_IS_OPEN(&(p->smb))) {
+		JS_ReportError(cx, "FileBase is not open");
+		return JS_FALSE;
+	}
+
+	uintN argn = 0;
+	if(argn < argc) {
+		JSVALUE_TO_MSTRING(cx, argv[argn], filename, NULL);
+		HANDLE_PENDING(cx, filename);
+		argn++;
+	}
+	if(argn < argc && JSVAL_IS_OBJECT(argv[argn])) {
+		fileobj = JSVAL_TO_OBJECT(argv[argn]);
+		argn++;
+	}
+	if(argn < argc && JSVAL_IS_BOOLEAN(argv[argn])) {
+		use_diz_always = JSVAL_TO_BOOLEAN(argv[argn]);
+		argn++;
+	}
+
+	JSBool result = JS_TRUE;
+	char* extdesc = NULL;
+	rc=JS_SUSPENDREQUEST(cx);
+	if(filename != NULL && fileobj != NULL
+		&& (p->smb_result = smb_loadfile(&p->smb, filename, &file, file_detail_extdesc)) == SMB_SUCCESS) {
+		p->smb_result = parse_file_properties(cx, fileobj, &file, &extdesc);
+		if((extdesc == NULL	|| use_diz_always == true)
+			&& file.dir < scfg->total_dirs
+			&& (scfg->dir[file.dir]->misc & DIR_DIZ)) {
+			get_diz(scfg, &file, &extdesc);
+		}
+		if(p->smb_result == SMB_SUCCESS) {
+			char orgfname[MAX_PATH + 1];
+			char newfname[MAX_PATH + 1];
+			getfilepath(scfg, &file, newfname);
+			SAFECOPY(orgfname, newfname);
+			*getfname(orgfname) = '\0';
+			SAFECAT(orgfname, filename);
+			if(strcmp(orgfname, newfname) != 0 && fexistcase(orgfname) && rename(orgfname, newfname) != 0) {
+				JS_ReportError(cx, "%d renaming '%s' to '%s'", errno, orgfname, newfname);
+				result = JS_FALSE;
+				p->smb_result = SMB_ERR_RENAME;
+			} else {
+				if(file.extdesc != NULL)
+					truncsp(file.extdesc);
+				if(strcmp(extdesc ? extdesc : "", file.extdesc ? file.extdesc : "") == 0)
+					p->smb_result = smb_putfile(&p->smb, &file);
+				else {
+					if((p->smb_result = smb_removefile(&p->smb, &file)) == SMB_SUCCESS)
+						p->smb_result = smb_addfile(&p->smb, &file, SMB_SELFPACK, extdesc, newfname);
+				}
+			}
+		}
+		JS_SET_RVAL(cx, arglist, BOOLEAN_TO_JSVAL(p->smb_result == SMB_SUCCESS));
+	}
+	JS_RESUMEREQUEST(cx, rc);
+	smb_freefilemem(&file);
+	free(filename);
+	free(extdesc);
+
+	return result;
+}
+
+static JSBool
+js_renew_file(JSContext *cx, uintN argc, jsval *arglist)
+{
+	JSObject*	obj = JS_THIS_OBJECT(cx, arglist);
+	jsval*		argv = JS_ARGV(cx, arglist);
+	private_t*	p;
+	char*		fname = NULL;
+	jsrefcount	rc;
+
+	JS_SET_RVAL(cx, arglist, JSVAL_FALSE);
+
+	scfg_t* scfg = JS_GetRuntimePrivate(JS_GetRuntime(cx));
+	if(scfg == NULL) {
+		JS_ReportError(cx, "JS_GetRuntimePrivate returned NULL");
+		return JS_FALSE;
+	}
+
+	if((p=(private_t*)js_GetClassPrivate(cx, obj, &js_filebase_class))==NULL)
+		return JS_FALSE;
+
+	if(!SMB_IS_OPEN(&(p->smb))) {
+		JS_ReportError(cx, "FileBase is not open");
+		return JS_FALSE;
+	}
+
+	uintN argn = 0;
+	if(argn < argc) {
+		JSVALUE_TO_MSTRING(cx, argv[argn], fname, NULL);
+		HANDLE_PENDING(cx, fname);
+		argn++;
+	}
+	if(fname == NULL)
+		return JS_TRUE;
+
+	rc=JS_SUSPENDREQUEST(cx);
+	file_t file;
+	if((p->smb_result = smb_loadfile(&p->smb, fname, &file, file_detail_index)) == SMB_SUCCESS) {
+		char path[MAX_PATH + 1];
+		p->smb_result = smb_renewfile(&p->smb, &file, SMB_SELFPACK, getfilepath(scfg, &file, path));
+		smb_freefilemem(&file);
+	}
+	JS_SET_RVAL(cx, arglist, BOOLEAN_TO_JSVAL(p->smb_result == SMB_SUCCESS));
+	JS_RESUMEREQUEST(cx, rc);
+	free(fname);
+
+	return JS_TRUE;
+}
+
+static JSBool
+js_remove_file(JSContext *cx, uintN argc, jsval *arglist)
+{
+	JSObject*	obj = JS_THIS_OBJECT(cx, arglist);
+	jsval*		argv = JS_ARGV(cx, arglist);
+	private_t*	p;
+	char*		fname = NULL;
+	bool		delfile = false;
+	jsrefcount	rc;
+
+	JS_SET_RVAL(cx, arglist, JSVAL_FALSE);
+
+	scfg_t* scfg = JS_GetRuntimePrivate(JS_GetRuntime(cx));
+	if(scfg == NULL) {
+		JS_ReportError(cx, "JS_GetRuntimePrivate returned NULL");
+		return JS_FALSE;
+	}
+
+	if((p=(private_t*)js_GetClassPrivate(cx, obj, &js_filebase_class))==NULL)
+		return JS_FALSE;
+
+	if(!SMB_IS_OPEN(&(p->smb))) {
+		JS_ReportError(cx, "FileBase is not open");
+		return JS_FALSE;
+	}
+
+	uintN argn = 0;
+	if(argn < argc)	{
+		JSVALUE_TO_MSTRING(cx, argv[argn], fname, NULL);
+		HANDLE_PENDING(cx, fname);
+		argn++;
+	}
+	if(argn < argc && JSVAL_IS_BOOLEAN(argv[argn])) {
+		delfile = JSVAL_TO_BOOLEAN(argv[argn]);
+		argn++;
+	}
+	if(fname == NULL)
+		return JS_TRUE;
+
+	JSBool result = JS_TRUE;
+	rc=JS_SUSPENDREQUEST(cx);
+	file_t file;
+	if((p->smb_result = smb_loadfile(&p->smb, fname, &file, file_detail_index)) == SMB_SUCCESS) {
+		char path[MAX_PATH + 1];
+		if(delfile && remove(getfilepath(scfg, &file, path)) != 0) {
+			JS_ReportError(cx, "%d removing '%s'", errno, path);
+			p->smb_result = SMB_ERR_DELETE;
+			result = JS_FALSE;
+		} else
+			p->smb_result = smb_removefile(&p->smb, &file);
+		smb_freefilemem(&file);
+	}
+	JS_SET_RVAL(cx, arglist, BOOLEAN_TO_JSVAL(p->smb_result == SMB_SUCCESS));
+	JS_RESUMEREQUEST(cx, rc);
+	free(fname);
+
+	return result;
+}
+
+/* FileBase Object Properties */
+enum {
+	 FB_PROP_LAST_ERROR
+	,FB_PROP_FILE
+	,FB_PROP_DEBUG
+	,FB_PROP_RETRY_TIME
+	,FB_PROP_RETRY_DELAY
+	,FB_PROP_FIRST_FILE		/* first file number */
+	,FB_PROP_LAST_FILE		/* last file number */
+	,FB_PROP_LAST_FILE_TIME	/* last file index time */
+	,FB_PROP_FILES 			/* total files */
+	,FB_PROP_UPDATE_TIME
+    ,FB_PROP_MAX_FILES		/* Maximum number of file to keep in dir */
+    ,FB_PROP_MAX_AGE		/* Maximum age of file to keep in dir (in days) */
+	,FB_PROP_ATTR			/* Attributes for this file base (SMB_HYPER,etc) */
+	,FB_PROP_DIRNUM			/* Directory number */
+	,FB_PROP_IS_OPEN
+	,FB_PROP_STATUS			/* Last SMBLIB returned status value (e.g. retval) */
+};
+
+static JSBool js_filebase_set(JSContext *cx, JSObject *obj, jsid id, JSBool strict, jsval *vp)
+{
+	time32_t t;
+	jsval idval;
+    jsint       tiny;
+	private_t*	p;
+
+	if((p=(private_t*)js_GetClassPrivate(cx, obj, &js_filebase_class))==NULL) {
+		return JS_FALSE;
+	}
+
+    JS_IdToValue(cx, id, &idval);
+    tiny = JSVAL_TO_INT(idval);
+
+	switch(tiny) {
+		case FB_PROP_RETRY_TIME:
+			if(!JS_ValueToInt32(cx,*vp,(int32*)&(p->smb).retry_time))
+				return JS_FALSE;
+			break;
+		case FB_PROP_RETRY_DELAY:
+			if(!JS_ValueToInt32(cx,*vp,(int32*)&(p->smb).retry_delay))
+				return JS_FALSE;
+			break;
+		case FB_PROP_UPDATE_TIME:
+			if(!JS_ValueToInt32(cx, *vp, (int32*)&t))
+				return JS_FALSE;
+			update_newfiletime(&(p->smb), t);
+			break;
+	}
+
+	return JS_TRUE;
+}
+
+static JSBool js_filebase_get(JSContext *cx, JSObject *obj, jsid id, jsval *vp)
+{
+	jsval idval;
+	char*		s=NULL;
+	JSString*	js_str;
+    jsint       tiny;
+	idxrec_t	idx;
+	private_t*	p;
+	jsrefcount	rc;
+
+	if((p=(private_t*)JS_GetPrivate(cx,obj))==NULL)
+		return JS_FALSE;
+
+    JS_IdToValue(cx, id, &idval);
+    tiny = JSVAL_TO_INT(idval);
+
+	switch(tiny) {
+		case FB_PROP_FILE:
+			s=p->smb.file;
+			break;
+		case FB_PROP_LAST_ERROR:
+			s=p->smb.last_error;
+			break;
+		case FB_PROP_STATUS:
+			*vp = INT_TO_JSVAL(p->smb_result);
+			break;
+		case FB_PROP_RETRY_TIME:
+			*vp = INT_TO_JSVAL(p->smb.retry_time);
+			break;
+		case FB_PROP_RETRY_DELAY:
+			*vp = INT_TO_JSVAL(p->smb.retry_delay);
+			break;
+		case FB_PROP_FIRST_FILE:
+			rc=JS_SUSPENDREQUEST(cx);
+			memset(&idx,0,sizeof(idx));
+			smb_getfirstidx(&(p->smb),&idx);
+			JS_RESUMEREQUEST(cx, rc);
+			*vp=UINT_TO_JSVAL(idx.number);
+			break;
+		case FB_PROP_LAST_FILE:
+			rc=JS_SUSPENDREQUEST(cx);
+			smb_getstatus(&(p->smb));
+			JS_RESUMEREQUEST(cx, rc);
+			*vp=UINT_TO_JSVAL(p->smb.status.last_file);
+			break;
+		case FB_PROP_LAST_FILE_TIME:
+			rc=JS_SUSPENDREQUEST(cx);
+			*vp = UINT_TO_JSVAL((uint32_t)lastfiletime(&p->smb));
+			JS_RESUMEREQUEST(cx, rc);
+			break;
+		case FB_PROP_FILES:
+			rc=JS_SUSPENDREQUEST(cx);
+			smb_getstatus(&(p->smb));
+			JS_RESUMEREQUEST(cx, rc);
+			*vp=UINT_TO_JSVAL(p->smb.status.total_files);
+			break;
+		case FB_PROP_UPDATE_TIME:
+			*vp = UINT_TO_JSVAL((uint32_t)newfiletime(&p->smb));
+			break;
+		case FB_PROP_MAX_FILES:
+			*vp=UINT_TO_JSVAL(p->smb.status.max_files);
+			break;
+		case FB_PROP_MAX_AGE:
+			*vp=UINT_TO_JSVAL(p->smb.status.max_age);
+			break;
+		case FB_PROP_ATTR:
+			*vp=UINT_TO_JSVAL(p->smb.status.attr);
+			break;
+		case FB_PROP_DIRNUM:
+			*vp = INT_TO_JSVAL(p->smb.dirnum);
+			break;
+		case FB_PROP_IS_OPEN:
+			*vp = BOOLEAN_TO_JSVAL(SMB_IS_OPEN(&(p->smb)));
+			break;
+	}
+
+	if(s!=NULL) {
+		if((js_str=JS_NewStringCopyZ(cx, s))==NULL)
+			return JS_FALSE;
+		*vp = STRING_TO_JSVAL(js_str);
+	}
+
+	return JS_TRUE;
+}
+
+#define FB_PROP_FLAGS JSPROP_ENUMERATE|JSPROP_READONLY
+
+static jsSyncPropertySpec js_filebase_properties[] = {
+/*		 name				,tinyid					,flags,				ver	*/
+
+	{	"error"				,FB_PROP_LAST_ERROR		,FB_PROP_FLAGS,		31900 },
+	{	"last_error"		,FB_PROP_LAST_ERROR		,JSPROP_READONLY,	31900 },	/* alias */
+	{	"status"			,FB_PROP_STATUS			,FB_PROP_FLAGS,		31900 },
+	{	"file"				,FB_PROP_FILE			,FB_PROP_FLAGS,		31900 },
+	{	"debug"				,FB_PROP_DEBUG			,0,					31900 },
+	{	"retry_time"		,FB_PROP_RETRY_TIME		,JSPROP_ENUMERATE,	31900 },
+	{	"retry_delay"		,FB_PROP_RETRY_DELAY	,JSPROP_ENUMERATE,	31900 },
+	{	"first_file"		,FB_PROP_FIRST_FILE		,FB_PROP_FLAGS,		31900 },
+	{	"last_file"			,FB_PROP_LAST_FILE		,FB_PROP_FLAGS,		31900 },
+	{	"last_file_time"	,FB_PROP_LAST_FILE_TIME	,FB_PROP_FLAGS,		31900 },
+	{	"files"				,FB_PROP_FILES			,FB_PROP_FLAGS,		31900 },
+	{	"update_time"		,FB_PROP_UPDATE_TIME	,JSPROP_ENUMERATE,	31900 },
+	{	"max_files"			,FB_PROP_MAX_FILES  	,FB_PROP_FLAGS,		31900 },
+	{	"max_age"			,FB_PROP_MAX_AGE   		,FB_PROP_FLAGS,		31900 },
+	{	"attributes"		,FB_PROP_ATTR			,FB_PROP_FLAGS,		31900 },
+	{	"dirnum"			,FB_PROP_DIRNUM			,FB_PROP_FLAGS,		31900 },
+	{	"is_open"			,FB_PROP_IS_OPEN		,FB_PROP_FLAGS,		31900 },
+	{0}
+};
+
+#ifdef BUILD_JSDOCS
+static char* filebase_prop_desc[] = {
+
+	 "last occurred file base error - <small>READ ONLY</small>"
+	,"return value of last <i>SMB Library</i> function call - <small>READ ONLY</small>"
+	,"base path and filename of file base - <small>READ ONLY</small>"
+	,"file base open/lock retry timeout (in seconds)"
+	,"delay between file base open/lock retries (in milliseconds)"
+	,"first file number - <small>READ ONLY</small>"
+	,"last file number - <small>READ ONLY</small>"
+	,"timestamp of last file - <small>READ ONLY</small>"
+	,"total number of files - <small>READ ONLY</small>"
+	,"timestamp of file base index (only writable when file base is closed)"
+	,"maximum number of files before expiration - <small>READ ONLY</small>"
+	,"maximum age (in days) of files to store - <small>READ ONLY</small>"
+	,"file base attributes - <small>READ ONLY</small>"
+	,"directory number (0-based, -1 if invalid) - <small>READ ONLY</small>"
+	,"<i>true</i> if the file base has been opened successfully - <small>READ ONLY</small>"
+	,NULL
+};
+#endif
+
+static jsSyncMethodSpec js_filebase_functions[] = {
+	{"open",			js_open,			0, JSTYPE_BOOLEAN
+		,JSDOCSTR("")
+		,JSDOCSTR("open file base")
+		,31900
+	},
+	{"close",			js_close,			0, JSTYPE_BOOLEAN
+		,JSDOCSTR("")
+		,JSDOCSTR("close file base (if open)")
+		,31900
+	},
+	{"get",				js_get_file,		2, JSTYPE_OBJECT
+		,JSDOCSTR("filename or file-meta-object [,detail=FileBase.DETAIL.NORM]")
+		,JSDOCSTR("get a file metadata object")
+		,31900
+	},
+	{"get_list",		js_get_file_list,	4, JSTYPE_ARRAY
+		,JSDOCSTR("[filespec] [,detail=FileBase.DETAIL.NORM] [,since-time=0] [,sort=true [,order]]")
+		,JSDOCSTR("get a list (array) of file metadata objects"
+			", the default sort order is the sysop-configured order or <tt>FileBase.SORT.NAME_AI</tt>"
+			)
+		,31900
+	},
+	{"get_name",		js_get_file_name,	1, JSTYPE_STRING
+		,JSDOCSTR("path/filename")
+		,JSDOCSTR("returns index-formatted (e.g. shortened) version of filename without path (file base does not have to be open)")
+		,31900
+	},
+	{"get_names",		js_get_file_names,	3, JSTYPE_ARRAY
+		,JSDOCSTR("[filespec] [,since-time=0] [,sort=true [,order]]")
+		,JSDOCSTR("get a list of index-formatted (e.g. shortened) filenames (strings) from file base index"
+			", the default sort order is the sysop-configured order or <tt>FileBase.SORT.NAME_A</tt>")
+		,31900
+	},
+	{"get_path",		js_get_file_path,	1, JSTYPE_STRING
+		,JSDOCSTR("filename")
+		,JSDOCSTR("get the full path to the local file")
+		,31900
+	},
+	{"get_size",		js_get_file_size,	1, JSTYPE_NUMBER
+		,JSDOCSTR("filename")
+		,JSDOCSTR("get the size of the local file, in bytes, or -1 if it does not exist")
+		,31900
+	},
+	{"get_time",		js_get_file_time,	1, JSTYPE_NUMBER
+		,JSDOCSTR("filename")
+		,JSDOCSTR("get the modification date/time stamp of the local file")
+		,31900
+	},
+	{"add",				js_add_file,		1, JSTYPE_BOOLEAN
+		,JSDOCSTR("file-meta-object [,use_diz_always=false]")
+		,JSDOCSTR("add a file to the file base")
+		,31900
+	},
+	{"remove",			js_remove_file,		2, JSTYPE_BOOLEAN
+		,JSDOCSTR("filename [,delete=false]")
+		,JSDOCSTR("remove an existing file from the file base and optionally delete file"
+				", may throw exception on errors (e.g. file remove failure)")
+		,31900
+	},
+	{"update",			js_update_file,		3, JSTYPE_BOOLEAN
+		,JSDOCSTR("filename, file-meta-object [,use_diz_always=false]")
+		,JSDOCSTR("update an existing file in the file base"
+				", may throw exception on errors (e.g. file rename failure)")
+		,31900
+	},
+	{"renew",			js_renew_file,		1, JSTYPE_BOOLEAN
+		,JSDOCSTR("filename")
+		,JSDOCSTR("remove and re-add (as new) an existing file in the file base")
+		,31900
+	},
+	{"hash",			js_hash_file,		1, JSTYPE_OBJECT
+		,JSDOCSTR("filename_or_fullpath")
+		,JSDOCSTR("calculate hashes of a file's contents (file base does not have to be open)")
+		,31900
+	},
+	{"dump",			js_dump_file,		1, JSTYPE_ARRAY
+		,JSDOCSTR("filename")
+		,JSDOCSTR("dump file metadata to an array of strings for diagnostic uses")
+		,31900
+	},
+	{0}
+};
+
+static JSBool js_filebase_resolve(JSContext *cx, JSObject *obj, jsid id)
+{
+	char*			name=NULL;
+	JSBool			ret;
+
+	if(id != JSID_VOID && id != JSID_EMPTY) {
+		jsval idval;
+
+		JS_IdToValue(cx, id, &idval);
+		if(JSVAL_IS_STRING(idval)) {
+			JSVALUE_TO_MSTRING(cx, idval, name, NULL);
+			HANDLE_PENDING(cx, name);
+		}
+	}
+
+	ret=js_SyncResolve(cx, obj, name, js_filebase_properties, js_filebase_functions, NULL, 0);
+	if(name)
+		free(name);
+	return ret;
+}
+
+static JSBool js_filebase_enumerate(JSContext *cx, JSObject *obj)
+{
+	return(js_filebase_resolve(cx, obj, JSID_VOID));
+}
+
+JSClass js_filebase_class = {
+     "FileBase"				/* name			*/
+    ,JSCLASS_HAS_PRIVATE	/* flags		*/
+	,JS_PropertyStub		/* addProperty	*/
+	,JS_PropertyStub		/* delProperty	*/
+	,js_filebase_get		/* getProperty	*/
+	,js_filebase_set		/* setProperty	*/
+	,js_filebase_enumerate	/* enumerate	*/
+	,js_filebase_resolve	/* resolve		*/
+	,JS_ConvertStub			/* convert		*/
+	,js_finalize_filebase	/* finalize		*/
+};
+
+/* FileBase Constructor (open file base) */
+static JSBool
+js_filebase_constructor(JSContext *cx, uintN argc, jsval *arglist)
+{
+	JSObject *		obj;
+	jsval *			argv=JS_ARGV(cx, arglist);
+	char*			base = NULL;
+	private_t*		p;
+	scfg_t*			scfg;
+
+	scfg=JS_GetRuntimePrivate(JS_GetRuntime(cx));
+
+	obj=JS_NewObject(cx, &js_filebase_class, NULL, NULL);
+	JS_SET_RVAL(cx, arglist, OBJECT_TO_JSVAL(obj));
+	if((p=(private_t*)malloc(sizeof(private_t)))==NULL) {
+		JS_ReportError(cx,"malloc failed");
+		return JS_FALSE;
+	}
+
+	memset(p,0,sizeof(private_t));
+	p->smb.retry_time=scfg->smb_retry_time;
+
+	JSSTRING_TO_MSTRING(cx, JS_ValueToString(cx, argv[0]), base, NULL);
+	if(JS_IsExceptionPending(cx)) {
+		if(base != NULL)
+			free(base);
+		free(p);
+		return JS_FALSE;
+	}
+	if(base==NULL) {
+		JS_ReportError(cx, "Invalid base parameter");
+		free(p);
+		return JS_FALSE;
+	}
+
+	if(!JS_SetPrivate(cx, obj, p)) {
+		JS_ReportError(cx,"JS_SetPrivate failed");
+		free(p);
+		free(base);
+		return JS_FALSE;
+	}
+
+#ifdef BUILD_JSDOCS
+	js_DescribeSyncObject(cx,obj,"Class used for accessing file databases", 31900);
+	js_DescribeSyncConstructor(cx,obj,"To create a new FileBase object: "
+		"<tt>var filebase = new FileBase('<i>code</i>')</tt><br>"
+		"where <i>code</i> is a directory internal code."
+		);
+	js_CreateArrayOfStrings(cx, obj, "_property_desc_list", filebase_prop_desc, JSPROP_READONLY);
+#endif
+
+	p->smb.dirnum = getdirnum(scfg, base);
+	if(p->smb.dirnum >= 0 && p->smb.dirnum < scfg->total_dirs) {
+		safe_snprintf(p->smb.file, sizeof(p->smb.file), "%s%s"
+			,scfg->dir[p->smb.dirnum]->data_dir, scfg->dir[p->smb.dirnum]->code);
+	} else { /* unknown code */
+		SAFECOPY(p->smb.file, base);
+	}
+
+	free(base);
+	return JS_TRUE;
+}
+
+#ifdef BUILD_JSDOCS
+static char* filebase_detail_prop_desc[] = {
+	 "Include indexed-filenames only",
+	 "Normal level of file detail (e.g. full filenames, minimal meta data)",
+	 "Normal level of file detail plus extended descriptions",
+	 "Maximum file detail, include undefined/null property values",
+	 NULL
+};
+
+static char* filebase_sort_prop_desc[] = {
+	"Natural sort order (same as DATE_A)",
+	"Filename ascending, case insensitive sort order",
+	"Filename descending, case insensitive sort order",
+	"Filename ascending, case sensitive sort order",
+	"Filename descending, case sensitive sort order",
+	"Import date/time ascending sort order",
+	"Import date/time descending sort order",
+	NULL
+};
+#endif
+
+JSObject* DLLCALL js_CreateFileBaseClass(JSContext* cx, JSObject* parent, scfg_t* cfg)
+{
+	JSObject*	obj;
+	JSObject*	constructor;
+	jsval		val;
+
+	obj = JS_InitClass(cx, parent, NULL
+		,&js_filebase_class
+		,js_filebase_constructor
+		,1	/* number of constructor args */
+		,NULL
+		,NULL
+		,NULL, NULL);
+
+	if(JS_GetProperty(cx, parent, js_filebase_class.name, &val) && !JSVAL_NULL_OR_VOID(val)) {
+		JS_ValueToObject(cx, val, &constructor);
+		JSObject* detail = JS_DefineObject(cx, constructor, "DETAIL", NULL, NULL, JSPROP_PERMANENT|JSPROP_ENUMERATE|JSPROP_READONLY);
+		if(detail != NULL) {
+			JS_DefineProperty(cx, detail, "MIN", INT_TO_JSVAL(file_detail_index), NULL, NULL
+				, JSPROP_PERMANENT|JSPROP_ENUMERATE|JSPROP_READONLY);
+			JS_DefineProperty(cx, detail, "NORM", INT_TO_JSVAL(file_detail_normal), NULL, NULL
+				, JSPROP_PERMANENT|JSPROP_ENUMERATE|JSPROP_READONLY);
+			JS_DefineProperty(cx, detail, "EXTENDED", INT_TO_JSVAL(file_detail_extdesc), NULL, NULL
+				, JSPROP_PERMANENT|JSPROP_ENUMERATE|JSPROP_READONLY);
+			JS_DefineProperty(cx, detail, "MAX", INT_TO_JSVAL(file_detail_extdesc + 1), NULL, NULL
+				, JSPROP_PERMANENT|JSPROP_ENUMERATE|JSPROP_READONLY);
+#ifdef BUILD_JSDOCS
+			js_DescribeSyncObject(cx, detail, "Detail level numeric constants", 0);
+			js_CreateArrayOfStrings(cx, detail, "_property_desc_list", filebase_detail_prop_desc, JSPROP_READONLY);
+#endif
+		}
+		JSObject* sort = JS_DefineObject(cx, constructor, "SORT", NULL, NULL, JSPROP_PERMANENT|JSPROP_ENUMERATE|JSPROP_READONLY);
+		if(sort != NULL) {
+			JS_DefineProperty(cx, sort, "NATURAL", INT_TO_JSVAL(FILE_SORT_NATURAL), NULL, NULL
+				, JSPROP_PERMANENT|JSPROP_ENUMERATE|JSPROP_READONLY);
+			JS_DefineProperty(cx, sort, "NAME_AI", INT_TO_JSVAL(FILE_SORT_NAME_A), NULL, NULL
+				, JSPROP_PERMANENT|JSPROP_ENUMERATE|JSPROP_READONLY);
+			JS_DefineProperty(cx, sort, "NAME_DI", INT_TO_JSVAL(FILE_SORT_NAME_D), NULL, NULL
+				, JSPROP_PERMANENT|JSPROP_ENUMERATE|JSPROP_READONLY);
+			JS_DefineProperty(cx, sort, "NAME_AS", INT_TO_JSVAL(FILE_SORT_NAME_AC), NULL, NULL
+				, JSPROP_PERMANENT|JSPROP_ENUMERATE|JSPROP_READONLY);
+			JS_DefineProperty(cx, sort, "NAME_DS", INT_TO_JSVAL(FILE_SORT_NAME_DC), NULL, NULL
+				, JSPROP_PERMANENT|JSPROP_ENUMERATE|JSPROP_READONLY);
+			JS_DefineProperty(cx, sort, "DATE_A", INT_TO_JSVAL(FILE_SORT_DATE_A), NULL, NULL
+				, JSPROP_PERMANENT|JSPROP_ENUMERATE|JSPROP_READONLY);
+			JS_DefineProperty(cx, sort, "DATE_D", INT_TO_JSVAL(FILE_SORT_DATE_D), NULL, NULL
+				, JSPROP_PERMANENT|JSPROP_ENUMERATE|JSPROP_READONLY);
+#ifdef BUILD_JSDOCS
+			js_DescribeSyncObject(cx, sort, "Sort order numeric constants", 0);
+			js_CreateArrayOfStrings(cx, sort, "_property_desc_list", filebase_sort_prop_desc, JSPROP_READONLY);
+#endif
+		}
+	}
+	return obj;
+}
diff --git a/src/sbbs3/js_global.c b/src/sbbs3/js_global.c
index 32ac501e28b4ee96a5640303ea3f7f21a8bf438c..c48311f123ee8eb0753a202827d2f5a08e4c47bf 100644
--- a/src/sbbs3/js_global.c
+++ b/src/sbbs3/js_global.c
@@ -2629,7 +2629,50 @@ js_md5_calc(JSContext* cx, uintN argc, jsval* arglist)
 	free(inbuf);
 
 	if(hex)
-		MD5_hex((BYTE*)outbuf,digest);
+		MD5_hex(outbuf,digest);
+	else
+		b64_encode(outbuf,sizeof(outbuf),(char*)digest,sizeof(digest));
+	JS_RESUMEREQUEST(cx, rc);
+
+	js_str = JS_NewStringCopyZ(cx, outbuf);
+	if(js_str==NULL)
+		return(JS_FALSE);
+
+	JS_SET_RVAL(cx, arglist, STRING_TO_JSVAL(js_str));
+	return(JS_TRUE);
+}
+
+static JSBool
+js_sha1_calc(JSContext* cx, uintN argc, jsval* arglist)
+{
+	jsval *argv=JS_ARGV(cx, arglist);
+	BYTE		digest[SHA1_DIGEST_SIZE];
+	JSBool		hex=JS_FALSE;
+	size_t		inbuf_len;
+	char*		inbuf = NULL;
+	char		outbuf[64];
+	JSString*	js_str;
+	jsrefcount	rc;
+
+	JS_SET_RVAL(cx, arglist, JSVAL_NULL);
+
+	if(argc==0 || JSVAL_NULL_OR_VOID(argv[0]))
+		return(JS_TRUE);
+
+	JSVALUE_TO_MSTRING(cx, argv[0], inbuf, &inbuf_len);
+	HANDLE_PENDING(cx, inbuf);
+	if(inbuf==NULL)
+		return(JS_TRUE);
+
+	if(argc>1 && JSVAL_IS_BOOLEAN(argv[1]))
+		hex=JSVAL_TO_BOOLEAN(argv[1]);
+
+	rc=JS_SUSPENDREQUEST(cx);
+	SHA1_calc(digest,inbuf,inbuf_len);
+	free(inbuf);
+
+	if(hex)
+		SHA1_hex(outbuf,digest);
 	else
 		b64_encode(outbuf,sizeof(outbuf),(char*)digest,sizeof(digest));
 	JS_RESUMEREQUEST(cx, rc);
@@ -3603,10 +3646,7 @@ js_wildmatch(JSContext *cx, uintN argc, jsval *arglist)
 		JS_ValueToBoolean(cx, argv[argn++], &path);
 	
 	rc=JS_SUSPENDREQUEST(cx);
-	if(case_sensitive)
-		JS_SET_RVAL(cx, arglist, BOOLEAN_TO_JSVAL(wildmatch(fname, spec, path)));
-	else
-		JS_SET_RVAL(cx, arglist, BOOLEAN_TO_JSVAL(wildmatchi(fname, spec, path)));
+	JS_SET_RVAL(cx, arglist, BOOLEAN_TO_JSVAL(wildmatch(fname, spec, path, case_sensitive)));
 	free(fname);
 	if(spec != spec_def)
 		free(spec);
@@ -5027,6 +5067,10 @@ static jsSyncMethodSpec js_global_functions[] = {
 	,JSDOCSTR("calculate and return 128-bit MD5 digest of text string, result encoded in base64 (default) or hexadecimal")
 	,311
 	},
+	{"sha1_calc",		js_sha1_calc,		1,	JSTYPE_STRING,	JSDOCSTR("text [,hex=false]")
+	,JSDOCSTR("calculate and return 160-bit SHA-1 digest of text string, result encoded in base64 (default) or hexadecimal")
+	,31900
+	},
 	{"gethostbyname",	js_resolve_ip,		1,	JSTYPE_ALIAS },
 	{"resolve_ip",		js_resolve_ip,		1,	JSTYPE_STRING,	JSDOCSTR("hostname [,array=<tt>false</tt>]")
 	,JSDOCSTR("resolve IP address of specified hostname (AKA gethostbyname).  If <i>array</i> is true (added in 3.17), will return "
diff --git a/src/sbbs3/js_internal.c b/src/sbbs3/js_internal.c
index bb10abb702382493378fea0dbc88f0e609c85e83..82f51628cacc7739db73c98c90b0fe8e3f11ee6c 100644
--- a/src/sbbs3/js_internal.c
+++ b/src/sbbs3/js_internal.c
@@ -713,7 +713,7 @@ js_setTimeout(JSContext *cx, uintN argc, jsval *arglist)
 	js_callback_t*	cb;
 	JSObject *obj=JS_THIS_OBJECT(cx, arglist);
 	JSFunction *ecb;
-	uint64_t now = xp_timer() * 1000;
+	uint64_t now = (uint64_t)(xp_timer() * 1000);
 	jsdouble timeout;
 
 	if((cb=(js_callback_t*)JS_GetPrivate(cx,obj))==NULL)
@@ -752,7 +752,7 @@ js_setTimeout(JSContext *cx, uintN argc, jsval *arglist)
 	ev->cx = obj;
 	JS_AddObjectRoot(cx, &ev->cx);
 	ev->cb = ecb;
-	ev->data.timeout.end = now + timeout;
+	ev->data.timeout.end = (uint64_t)(now + timeout);
 	ev->id = cb->next_eid++;
 	cb->events = ev;
 
@@ -831,7 +831,7 @@ js_setInterval(JSContext *cx, uintN argc, jsval *arglist)
 	js_callback_t*	cb;
 	JSObject *obj=JS_THIS_OBJECT(cx, arglist);
 	JSFunction *ecb;
-	uint64_t now = xp_timer() * 1000;
+	uint64_t now = (uint64_t)(xp_timer() * 1000);
 	jsdouble period;
 
 	if((cb=(js_callback_t*)JS_GetPrivate(cx,obj))==NULL)
@@ -871,7 +871,7 @@ js_setInterval(JSContext *cx, uintN argc, jsval *arglist)
 	JS_AddObjectRoot(cx, &ev->cx);
 	ev->cb = ecb;
 	ev->data.interval.last = now;
-	ev->data.interval.period = period;
+	ev->data.interval.period =(uint64_t)period;
 	ev->id = cb->next_eid++;
 	cb->events = ev;
 
@@ -1090,7 +1090,7 @@ js_handle_events(JSContext *cx, js_callback_t *cb, volatile int *terminated)
 
 	while (cb->keepGoing && !JS_IsExceptionPending(cx) && cb->events && !*terminated) {
 		timeout = -1;	// Infinity by default...
-		now = xp_timer() * 1000;
+		now = (uint64_t)(xp_timer() * 1000);
 		ev = NULL;
 		tev = NULL;
 		cev = NULL;
@@ -1170,7 +1170,7 @@ js_handle_events(JSContext *cx, js_callback_t *cb, volatile int *terminated)
 						tev = ev;
 					}
 					else {
-						i = ev->data.interval.last + ev->data.interval.period - now;
+						i = (int)(ev->data.interval.last + ev->data.interval.period - now);
 						if (timeout == -1 || i < timeout) {
 							timeout = i;
 							tev = ev;
@@ -1184,7 +1184,7 @@ js_handle_events(JSContext *cx, js_callback_t *cb, volatile int *terminated)
 						tev = ev;
 					}
 					else {
-						i = ev->data.timeout.end - now;
+						i = (int)(ev->data.timeout.end - now);
 						if (timeout == -1 || i < timeout) {
 							timeout = i;
 							tev = ev;
diff --git a/src/sbbs3/js_msgbase.c b/src/sbbs3/js_msgbase.c
index 67802039352ed7ccc79008ece14310abfd100b4a..0febc40e8536d775d276ca292c9885281bdc997c 100644
--- a/src/sbbs3/js_msgbase.c
+++ b/src/sbbs3/js_msgbase.c
@@ -1042,7 +1042,7 @@ static BOOL msg_offset_by_id(private_t* p, char* id, int32_t* offset)
 	if((p->smb_result = smb_getmsgidx_by_msgid(&(p->smb),&msg,id))!=SMB_SUCCESS)
 		return(FALSE);
 
-	*offset = msg.offset;
+	*offset = msg.idx_offset;
 	return(TRUE);
 }
 
@@ -1135,7 +1135,7 @@ js_get_msg_index(JSContext *cx, uintN argc, jsval *arglist)
 			include_votes = JSVAL_TO_BOOLEAN(argv[n]);
 		else if(JSVAL_IS_NUMBER(argv[n])) {
 			if(by_offset) {							/* Get by offset */
-				if(!JS_ValueToInt32(cx, argv[n], (int32*)&msg.offset))
+				if(!JS_ValueToInt32(cx, argv[n], (int32*)&msg.idx_offset))
 					return JS_FALSE;
 			}
 			else {									/* Get by number */
@@ -1167,7 +1167,7 @@ js_get_msg_index(JSContext *cx, uintN argc, jsval *arglist)
 	if((idxobj=JS_NewObject(cx,NULL,proto,obj))==NULL)
 		return JS_TRUE;
 
-	set_msg_idx_properties(cx, idxobj, &msg.idx, msg.offset);
+	set_msg_idx_properties(cx, idxobj, &msg.idx, msg.idx_offset);
 
 	JS_SET_RVAL(cx, arglist, OBJECT_TO_JSVAL(idxobj));
 
@@ -1209,7 +1209,7 @@ js_get_index(JSContext *cx, uintN argc, jsval *arglist)
 	}
     JS_SET_RVAL(cx, arglist, OBJECT_TO_JSVAL(array));
 
-	uint32_t total_msgs = index_length / sizeof(*idx);
+	uint32_t total_msgs = (uint32_t)(index_length / sizeof(*idx));
 	if(total_msgs > priv->smb.status.total_msgs)
 		total_msgs = priv->smb.status.total_msgs;
 	if(total_msgs < 1) {
@@ -1381,7 +1381,7 @@ static JSBool js_get_msg_header_resolve(JSContext *cx, JSObject *obj, jsid id)
 	}
 
 	LAZY_UINTEGER("number", p->msg.hdr.number, JSPROP_ENUMERATE);
-	LAZY_UINTEGER("offset", p->msg.offset, JSPROP_ENUMERATE);
+	LAZY_UINTEGER("offset", p->msg.idx_offset, JSPROP_ENUMERATE);
 	LAZY_STRING_TRUNCSP("to",p->msg.to, JSPROP_ENUMERATE);
 	LAZY_STRING_TRUNCSP("from",p->msg.from, JSPROP_ENUMERATE);
 	LAZY_STRING_TRUNCSP("subject",p->msg.subj, JSPROP_ENUMERATE);
@@ -1740,7 +1740,7 @@ js_get_msg_header(JSContext *cx, uintN argc, jsval *arglist)
 	/* Now parse message offset/id and get message */
 	if(n < argc && JSVAL_IS_NUMBER(argv[n])) {
 		if(by_offset) {							/* Get by offset */
-			if(!JS_ValueToInt32(cx,argv[n++],(int32*)&(p->msg).offset)) {
+			if(!JS_ValueToInt32(cx,argv[n++],(int32*)&(p->msg).idx_offset)) {
 				free(p);
 				return JS_FALSE;
 			}
@@ -1884,7 +1884,7 @@ js_get_all_msg_headers(JSContext *cx, uintN argc, jsval *arglist)
 	}
     JS_SET_RVAL(cx, arglist, OBJECT_TO_JSVAL(retobj));
 
-	uint32_t total_msgs = index_length / sizeof(*idx);
+	uint32_t total_msgs = (uint32_t)(index_length / sizeof(*idx));
 	if(total_msgs > priv->smb.status.total_msgs)
 		total_msgs = priv->smb.status.total_msgs;
 	if(total_msgs < 1) {
@@ -2103,7 +2103,7 @@ js_put_msg_header(JSContext *cx, uintN argc, jsval *arglist)
 			by_offset=JSVAL_TO_BOOLEAN(argv[n]);
 		else if(JSVAL_IS_NUMBER(argv[n])) {
 			if(by_offset) {							/* Get by offset */
-				if(!JS_ValueToInt32(cx,argv[n],(int32*)&msg.offset))
+				if(!JS_ValueToInt32(cx,argv[n],(int32*)&msg.idx_offset))
 					return JS_FALSE;
 			}
 			else {									/* Get by number */
@@ -2116,7 +2116,7 @@ js_put_msg_header(JSContext *cx, uintN argc, jsval *arglist)
 			rc=JS_SUSPENDREQUEST(cx);
 			if(!msg_offset_by_id(p
 					,cstr
-					,&msg.offset)) {
+					,&msg.idx_offset)) {
 				free(cstr);
 				JS_RESUMEREQUEST(cx, rc);
 				return JS_TRUE;	/* ID not found */
@@ -2139,7 +2139,7 @@ js_put_msg_header(JSContext *cx, uintN argc, jsval *arglist)
 			JS_ReportError(cx, "Message header has 'expanded fields'");
 			return JS_FALSE;
 		}
-		msg.offset = mp->msg.offset;
+		msg.idx_offset = mp->msg.idx_offset;
 	}
 
 	rc=JS_SUSPENDREQUEST(cx);
@@ -2214,7 +2214,7 @@ js_remove_msg(JSContext *cx, uintN argc, jsval *arglist)
 			by_offset=JSVAL_TO_BOOLEAN(argv[n]);
 		else if(JSVAL_IS_NUMBER(argv[n])) {
 			if(by_offset) {							/* Get by offset */
-				if(!JS_ValueToInt32(cx,argv[n],(int32*)&msg.offset))
+				if(!JS_ValueToInt32(cx,argv[n],(int32*)&msg.idx_offset))
 					return JS_FALSE;
 			}
 			else {									/* Get by number */
@@ -2232,7 +2232,7 @@ js_remove_msg(JSContext *cx, uintN argc, jsval *arglist)
 			rc=JS_SUSPENDREQUEST(cx);
 			if(!msg_offset_by_id(p
 					,cstr
-					,&msg.offset)) {
+					,&msg.idx_offset)) {
 				free(cstr);
 				JS_RESUMEREQUEST(cx, rc);
 				return JS_TRUE;	/* ID not found */
@@ -2356,7 +2356,7 @@ js_get_msg_body(JSContext *cx, uintN argc, jsval *arglist)
 			by_offset=JSVAL_TO_BOOLEAN(argv[n]);
 		else if(JSVAL_IS_NUMBER(argv[n])) {
 			if(by_offset) {							/* Get by offset */
-				if(!JS_ValueToInt32(cx,argv[n],(int32*)&msg.offset))
+				if(!JS_ValueToInt32(cx,argv[n],(int32*)&msg.idx_offset))
 					return JS_FALSE;
 			}
 			else {									/* Get by number */
@@ -2372,7 +2372,7 @@ js_get_msg_body(JSContext *cx, uintN argc, jsval *arglist)
 			rc=JS_SUSPENDREQUEST(cx);
 			if(!msg_offset_by_id(p
 					,cstr
-					,&msg.offset)) {
+					,&msg.idx_offset)) {
 				free(cstr);
 				JS_RESUMEREQUEST(cx, rc);
 				return JS_TRUE;	/* ID not found */
@@ -2470,7 +2470,7 @@ js_get_msg_tail(JSContext *cx, uintN argc, jsval *arglist)
 			by_offset=JSVAL_TO_BOOLEAN(argv[n]);
 		else if(JSVAL_IS_NUMBER(argv[n])) {
 			if(by_offset) {							/* Get by offset */
-				if(!JS_ValueToInt32(cx,argv[n],(int32*)&msg.offset))
+				if(!JS_ValueToInt32(cx,argv[n],(int32*)&msg.idx_offset))
 					return JS_FALSE;
 			}
 			else {									/* Get by number */
@@ -2486,7 +2486,7 @@ js_get_msg_tail(JSContext *cx, uintN argc, jsval *arglist)
 			rc=JS_SUSPENDREQUEST(cx);
 			if(!msg_offset_by_id(p
 					,cstr
-					,&msg.offset)) {
+					,&msg.idx_offset)) {
 				free(cstr);
 				JS_RESUMEREQUEST(cx, rc);
 				return JS_TRUE;	/* ID not found */
diff --git a/src/sbbs3/js_socket.c b/src/sbbs3/js_socket.c
index f7006061a8e2ea6e602229c6dfe10a1ea749a554..f0156521f5748c23d7aaacb56c3e83e099ff2aa3 100644
--- a/src/sbbs3/js_socket.c
+++ b/src/sbbs3/js_socket.c
@@ -379,7 +379,7 @@ static int js_socket_sendfilesocket(js_socket_private_t *p, int file, off_t *off
 	int			i;
 
 	if(p->session==-1)
-		return sendfilesocket(p->sock, file, offset, count);
+		return (int)sendfilesocket(p->sock, file, offset, count);
 
 	len=filelength(file);
 
@@ -516,7 +516,7 @@ static ushort js_port(JSContext* cx, jsval val, int type)
 	return(0);
 }
 
-SOCKET DLLCALL js_socket(JSContext *cx, jsval val)
+SOCKET js_socket(JSContext *cx, jsval val)
 {
 	void*		vp;
 	JSClass*	cl;
@@ -536,7 +536,7 @@ SOCKET DLLCALL js_socket(JSContext *cx, jsval val)
 }
 
 #ifdef PREFER_POLL
-size_t DLLCALL js_socket_numsocks(JSContext *cx, jsval val)
+size_t js_socket_numsocks(JSContext *cx, jsval val)
 {
 	js_socket_private_t	*p;
 	JSClass*	cl;
@@ -571,7 +571,7 @@ size_t DLLCALL js_socket_numsocks(JSContext *cx, jsval val)
 	return ret;
 }
 
-size_t DLLCALL js_socket_add(JSContext *cx, jsval val, struct pollfd *fds, short events)
+size_t js_socket_add(JSContext *cx, jsval val, struct pollfd *fds, short events)
 {
 	js_socket_private_t	*p;
 	JSClass*	cl;
@@ -612,7 +612,7 @@ size_t DLLCALL js_socket_add(JSContext *cx, jsval val, struct pollfd *fds, short
 	return ret;
 }
 #else
-SOCKET DLLCALL js_socket_add(JSContext *cx, jsval val, fd_set *fds)
+SOCKET js_socket_add(JSContext *cx, jsval val, fd_set *fds)
 {
 	js_socket_private_t	*p;
 	JSClass*	cl;
@@ -648,7 +648,7 @@ SOCKET DLLCALL js_socket_add(JSContext *cx, jsval val, fd_set *fds)
 	return sock;
 }
 
-BOOL DLLCALL  js_socket_isset(JSContext *cx, jsval val, fd_set *fds)
+BOOL  js_socket_isset(JSContext *cx, jsval val, fd_set *fds)
 {
 	js_socket_private_t	*p;
 	JSClass*	cl;
@@ -685,7 +685,7 @@ BOOL DLLCALL  js_socket_isset(JSContext *cx, jsval val, fd_set *fds)
 	return FALSE;
 }
 
-void DLLCALL js_timeval(JSContext* cx, jsval val, struct timeval* tv)
+void js_timeval(JSContext* cx, jsval val, struct timeval* tv)
 {
 	jsdouble jsd;
 
@@ -700,7 +700,7 @@ void DLLCALL js_timeval(JSContext* cx, jsval val, struct timeval* tv)
 }
 #endif
 
-int DLLCALL js_polltimeout(JSContext* cx, jsval val)
+int js_polltimeout(JSContext* cx, jsval val)
 {
 	jsdouble jsd;
 
@@ -709,7 +709,7 @@ int DLLCALL js_polltimeout(JSContext* cx, jsval val)
 
 	if(JSVAL_IS_DOUBLE(val)) {
 		if(JS_ValueToNumber(cx,val,&jsd))
-			return jsd * 1000;
+			return (int)(jsd * 1000);
 	}
 
 	return 0;
@@ -2795,7 +2795,7 @@ static BOOL js_DefineSocketOptionsArray(JSContext *cx, JSObject *obj, int type)
 
 /* Socket Constructor (creates socket descriptor) */
 
-JSObject* DLLCALL js_CreateSocketObjectWithoutParent(JSContext* cx, SOCKET sock, CRYPT_CONTEXT session)
+JSObject* js_CreateSocketObjectWithoutParent(JSContext* cx, SOCKET sock, CRYPT_CONTEXT session)
 {
 	JSObject*	obj;
 	js_socket_private_t*	p;
@@ -3513,7 +3513,7 @@ js_socket_constructor(JSContext *cx, uintN argc, jsval *arglist)
 	return(JS_TRUE);
 }
 
-JSObject* DLLCALL js_CreateSocketClass(JSContext* cx, JSObject* parent)
+JSObject* js_CreateSocketClass(JSContext* cx, JSObject* parent)
 {
 	JSObject*	sockobj;
 	JSObject*	sockproto;
@@ -3563,7 +3563,7 @@ JSObject* DLLCALL js_CreateSocketClass(JSContext* cx, JSObject* parent)
 	return(sockobj);
 }
 
-JSObject* DLLCALL js_CreateSocketObject(JSContext* cx, JSObject* parent, char *name, SOCKET sock, CRYPT_CONTEXT session)
+JSObject* js_CreateSocketObject(JSContext* cx, JSObject* parent, char *name, SOCKET sock, CRYPT_CONTEXT session)
 {
 	JSObject*	obj;
 
@@ -3575,7 +3575,7 @@ JSObject* DLLCALL js_CreateSocketObject(JSContext* cx, JSObject* parent, char *n
 	return(obj);
 }
 
-JSObject* DLLCALL js_CreateSocketObjectFromSet(JSContext* cx, JSObject* parent, char *name, struct xpms_set *set)
+JSObject* js_CreateSocketObjectFromSet(JSContext* cx, JSObject* parent, char *name, struct xpms_set *set)
 {
 	JSObject*	obj;
 	js_socket_private_t*	p;
diff --git a/src/sbbs3/js_user.c b/src/sbbs3/js_user.c
index 39900e7d8463f837cc86d7ac8e2fc07a7b22940e..1d120b8b4cac75a551cf6baaecd22706c24dea3c 100644
--- a/src/sbbs3/js_user.c
+++ b/src/sbbs3/js_user.c
@@ -1334,7 +1334,7 @@ static jsSyncMethodSpec js_user_functions[] = {
 	{"get_time_left",	js_get_time_left,	1,	JSTYPE_NUMBER,	JSDOCSTR("start_time")
 	,JSDOCSTR("Returns the user's available remaining time online, in seconds,<br>"
 	"based on the passed <i>start_time</i> value (in time_t format)<br>"
-	"Note: this method does not account for pending forced timed events"
+	"Note: this method does not account for pending forced timed events<br>"
 	"Note: for the pre-defined user object on the BBS, you almost certainly want bbs.get_time_left() instead.")
 	,31401
 	},
diff --git a/src/sbbs3/jsdoor.c b/src/sbbs3/jsdoor.c
index d709c5514a5da449a7c059f87266032fee197b09..c07a0902d646e88aebaf62e383213907c813bc5b 100644
--- a/src/sbbs3/jsdoor.c
+++ b/src/sbbs3/jsdoor.c
@@ -284,6 +284,10 @@ BOOL DLLCALL js_CreateCommonObjects(JSContext* js_cx
 		if(js_CreateFileClass(js_cx, *glob)==NULL)
 			break;
 
+		/* Archive Class */
+		if(js_CreateArchiveClass(js_cx, *glob)==NULL)
+			break;
+
 		/* COM Class */
 		if(js_CreateCOMClass(js_cx, *glob)==NULL)
 			break;
diff --git a/src/sbbs3/listfile.cpp b/src/sbbs3/listfile.cpp
index 58923be8ead16e05a645677070fad2576f4babd8..79117cf7a3f828fc13b61308bd4240d52a68789d 100644
--- a/src/sbbs3/listfile.cpp
+++ b/src/sbbs3/listfile.cpp
@@ -1,9 +1,5 @@
-/* listfile.cpp */
-
 /* Synchronet file database listing functions */
 
-/* $Id: listfile.cpp,v 1.66 2020/05/11 08:57:18 rswindell Exp $ */
-
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
  * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
@@ -17,102 +13,62 @@
  * See the GNU General Public License for more details: gpl.txt or			*
  * http://www.fsf.org/copyleft/gpl.html										*
  *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
- *																			*
  * For Synchronet coding style and modification guidelines, see				*
  * http://www.synchro.net/source.html										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
 #include "sbbs.h"
+#include "filedat.h"
 
 #define BF_MAX	26	/* Batch Flag max: A-Z */	
 
 int extdesclines(char *str);
 
 /*****************************************************************************/
-/* List files in directory 'dir' that match 'filespec'. Filespec must be 	 */
-/* padded. ex: FILE*   .EXT, not FILE*.EXT. 'mode' determines other critiria */
+/* List files in directory 'dir' that match 'filespec'.						 */
+/* 'mode' determines other criteria											 */
 /* the files must meet before they'll be listed. 'mode' bit FL_NOHDR doesn't */
 /* list the directory header.                                                */
 /* Returns -1 if the listing was aborted, otherwise total files listed		 */
 /*****************************************************************************/
-int sbbs_t::listfiles(uint dirnum, const char *filespec, int tofile, long mode)
+int sbbs_t::listfiles(uint dirnum, const char *filespec, FILE* tofile, long mode)
 {
-	char	str[256],hdr[256],letter='A',*p,*datbuf,ext[513];
-	char 	tmp[512];
-	uchar*	ixbbuf;
+	char	hdr[256],letter='A',*p;
 	uchar	flagprompt=0;
 	int		c, d;
 	uint	i,j;
-	int		file,found=0,lastbat=0,disp;
-	long	m=0,n,anchor=0,next,datbuflen;
-	int32_t	l;
+	int		found=0,lastbat=0,disp;
+	size_t	m=0;
+	long	anchor=0,next;
+	file_t* bf[BF_MAX];	/* bf is batch flagged files */
+	smb_t	smb;
 	ulong	file_row[26];
-	file_t	f,bf[26];	/* bf is batch flagged files */
 
+	if(!smb_init_dir(&cfg, &smb, dirnum))
+		return 0;
 	if(mode&FL_ULTIME) {
-		last_ns_time=now;
-		sprintf(str,"%s%s.dab",cfg.dir[dirnum]->data_dir,cfg.dir[dirnum]->code);
-		if((file=nopen(str,O_RDONLY))!=-1) {
-			read(file,&l,4);
-			close(file);
-			if(ns_time>(time_t)l)
-				return(0); 
-		}
-	}
-	sprintf(str,"%s%s.ixb",cfg.dir[dirnum]->data_dir,cfg.dir[dirnum]->code);
-	if((file=nopen(str,O_RDONLY))==-1)
-		return(0);
-	l=(long)filelength(file);
-	if(!l) {
-		close(file);
-		return(0); 
-	}
-	if((ixbbuf=(uchar *)malloc(l))==NULL) {
-		close(file);
-		errormsg(WHERE,ERR_ALLOC,str,l);
-		return(0); 
-	}
-	if(lread(file,ixbbuf,l)!=l) {
-		close(file);
-		errormsg(WHERE,ERR_READ,str,l);
-		free((char *)ixbbuf);
-		return(0); 
-	}
-	close(file);
-	sprintf(str,"%s%s.dat",cfg.dir[dirnum]->data_dir,cfg.dir[dirnum]->code);
-	if((file=nopen(str,O_RDONLY))==-1) {
-		errormsg(WHERE,ERR_OPEN,str,O_RDONLY);
-		free((char *)ixbbuf);
-		return(0); 
+		last_ns_time = now;
+		if(!newfiles(&smb, ns_time))	// this is fast
+			return 0;
 	}
-	datbuflen=(long)filelength(file);
-	if((datbuf=(char *)malloc(datbuflen))==NULL) {
-		close(file);
-		errormsg(WHERE,ERR_ALLOC,str,datbuflen);
-		free((char *)ixbbuf);
-		return(0); 
-	}
-	if(lread(file,datbuf,datbuflen)!=datbuflen) {
-		close(file);
-		errormsg(WHERE,ERR_READ,str,datbuflen);
-		free((char *)datbuf);
-		free((char *)ixbbuf);
-		return(0); 
+	if(smb_open_dir(&cfg, &smb, dirnum) != SMB_SUCCESS)
+		return 0;
+
+	size_t file_count = 0;
+	file_t* file_list = loadfiles(&smb
+		, (mode&(FL_FINDDESC|FL_EXFIND)) ? NULL : filespec
+		, (mode&FL_ULTIME) ? ns_time : 0
+		, file_detail_extdesc
+		, (enum file_sort)cfg.dir[dirnum]->sort
+		, &file_count);
+	if(file_list == NULL || file_count < 1) {
+		smb_close(&smb);
+		free(file_list);
+		return 0;
 	}
-	close(file);
+
 	if(!tofile) {
 		action=NODE_LFIL;
 		getnodedat(cfg.node_num,&thisnode,0);
@@ -124,34 +80,35 @@ int sbbs_t::listfiles(uint dirnum, const char *filespec, int tofile, long mode)
 		} 
 	}
 
-	while(online && found<MAX_FILES) {
+	m = 0; // current file index
+	file_t* f;
+	while(online) {
 		if(found<0)
 			found=0;
-		if(m>=l || flagprompt) {		  /* End of list */
+		if(m>=file_count || flagprompt) {		  /* End of list */
 			if(useron.misc&BATCHFLAG && !tofile && found && found!=lastbat
 				&& !(mode&(FL_EXFIND|FL_VIEW))) {
 				flagprompt=0;
 				lncntr=0;
-				if((i=batchflagprompt(dirnum,bf,file_row,letter-'A',l/F_IXBSIZE))==2) {
+				if((i=batchflagprompt(&smb, bf, file_row, letter-'A', file_count))==2) {
 					m=anchor;
 					found-=letter-'A';
 					letter='A'; 
 				}
 				else if(i==3) {
-					if((long)anchor-((letter-'A')*F_IXBSIZE)<0) {
+					if((long)anchor-(letter-'A')<0) {
 						m=0;
 						found=0; 
 					}
 					else {
-						m=anchor-((letter-'A')*F_IXBSIZE);
+						m=anchor-(letter-'A');
 						found-=letter-'A'; 
 					}
 					letter='A'; 
 				}
 				else if((int)i==-1) {
-					free((char *)ixbbuf);
-					free((char *)datbuf);
-					return(-1); 
+					found = -1;
+					break;
 				}
 				else
 					break;
@@ -161,6 +118,8 @@ int sbbs_t::listfiles(uint dirnum, const char *filespec, int tofile, long mode)
 			else
 				break; 
 		}
+		if(m < file_count)
+			f = &file_list[m];
 
 		if(letter>'Z')
 			letter='A';
@@ -168,60 +127,47 @@ int sbbs_t::listfiles(uint dirnum, const char *filespec, int tofile, long mode)
 			anchor=m;
 
 		if(msgabort()) {		 /* used to be !tofile && msgabort() */
-			free((char *)ixbbuf);
-			free((char *)datbuf);
-			return(-1); 
+			found = -1;
+			break;
 		}
-		for(j=0;j<12 && m<l;j++)
-			if(j==8)
-				str[j]=ixbbuf[m]>' ' ? '.' : ' ';
-			else
-				str[j]=ixbbuf[m++];		/* Turns FILENAMEEXT into FILENAME.EXT */
-		str[j]=0;
+#if 0 /* unnecessary? */
 		if(!(mode&(FL_FINDDESC|FL_EXFIND)) && filespec[0]
 			&& !filematch(str,filespec)) {
 			m+=11;
 			continue; 
 		}
-		n=ixbbuf[m]|((long)ixbbuf[m+1]<<8)|((long)ixbbuf[m+2]<<16);
-		if(n>=datbuflen) {	/* out of bounds */
-			m+=11;
-			continue; 
-		}
+#endif
 		if(mode&(FL_FINDDESC|FL_EXFIND)) {
-			getrec((char *)&datbuf[n],F_DESC,LEN_FDESC,tmp);
-			strupr(tmp);
-			p=strstr(tmp,filespec);
+			p = (f->desc == NULL) ? NULL : strcasestr(f->desc, filespec);
 			if(!(mode&FL_EXFIND) && p==NULL) {
-				m+=11;
+				m++;
 				continue; 
 			}
-			getrec((char *)&datbuf[n],F_MISC,1,tmp);
-			j=tmp[0];  /* misc bits */
-			if(j) j-=' ';
-			if(mode&FL_EXFIND && j&FM_EXTDESC) { /* search extended description */
-				getextdesc(&cfg,dirnum,n,ext);
-				strupr(ext);
-				if(!strstr(ext,filespec) && !p) {	/* not in description or */
-					m+=11;						 /* extended description */
+			if(mode&FL_EXFIND && f->extdesc != NULL) { /* search extended description */
+				if(!strcasestr((char*)f->extdesc, filespec) && p == NULL) {	/* not in description or */
+					m++;											 /* extended description */
 					continue; 
 				}
 			}
-			else if(!p) {			 /* no extended description and not in desc */
-				m+=11;
-				continue; } 
+			else if(p == NULL) {			 /* no extended description and not in desc */
+				m++;
+				continue; 
+			} 
 		}
+/** necessary?
 		if(mode&FL_ULTIME) {
 			if(ns_time>(ixbbuf[m+3]|((long)ixbbuf[m+4]<<8)|((long)ixbbuf[m+5]<<16)
 				|((long)ixbbuf[m+6]<<24))) {
 				m+=11;
-				continue; } 
+				continue; 
+			}
 		}
+**/
 		if(useron.misc&BATCHFLAG && letter=='A' && found && !tofile
 			&& !(mode&(FL_EXFIND|FL_VIEW))
 			&& (!mode || !(useron.misc&EXPERT)))
 			bputs(text[FileListBatchCommands]);
-		m+=11;
+		m++;
 		if(!found && !(mode&(FL_EXFIND|FL_VIEW))) {
 			for(i=0;i<usrlibs;i++)
 				if(usrlib[i]==cfg.dir[dirnum]->lib)
@@ -244,61 +190,54 @@ int sbbs_t::listfiles(uint dirnum, const char *filespec, int tofile, long mode)
 							: strlen(cfg.dir[dirnum]->lname)+17;
 						if(i>8 || j>8) d++;
 						attr(cfg.color[clr_filelsthdrbox]);
-						bputs("��");            /* use to start with \r\n */
+						bputs("\xc9\xcd");            /* use to start with \r\n */
 						for(c=0;c<d;c++)
-							outchar('�');
-						bputs("�\r\n� ");
+							outchar('\xcd');
+						bputs("\xbb\r\n\xba ");
 						sprintf(hdr,text[BoxHdrLib],i+1,cfg.lib[usrlib[i]]->lname);
 						bputs(hdr);
 						for(c=bstrlen(hdr);c<d;c++)
 							outchar(' ');
 						attr(cfg.color[clr_filelsthdrbox]);
-						bputs("�\r\n� ");
+						bputs("\xba\r\n\xba ");
 						sprintf(hdr,text[BoxHdrDir],j+1,cfg.dir[dirnum]->lname);
 						bputs(hdr);
 						for(c=bstrlen(hdr);c<d;c++)
 							outchar(' ');
 						attr(cfg.color[clr_filelsthdrbox]);
-						bputs("�\r\n� ");
-						sprintf(hdr,text[BoxHdrFiles],l/F_IXBSIZE);
+						bputs("\xba\r\n\xba ");
+						sprintf(hdr,text[BoxHdrFiles], file_count);
 						bputs(hdr);
 						for(c=bstrlen(hdr);c<d;c++)
 							outchar(' ');
 						attr(cfg.color[clr_filelsthdrbox]);
-						bputs("�\r\n��");
+						bputs("\xba\r\n\xc8\xcd");
 						for(c=0;c<d;c++)
-							outchar('�');
-						bputs("�\r\n"); 
+							outchar('\xcd');
+						bputs("\xbc\r\n"); 
 					}
 				}
 			}
 			else {					/* short header */
 				if(tofile) {
-					sprintf(hdr,"(%u) %s ",i+1,cfg.lib[usrlib[i]]->sname);
-					write(tofile,crlf,2);
-					write(tofile,hdr,strlen(hdr)); 
+					c = fprintf(tofile,"\r\n(%u) %s ",i+1,cfg.lib[usrlib[i]]->sname) - 2;
 				}
 				else {
 					sprintf(hdr,text[ShortHdrLib],i+1,cfg.lib[usrlib[i]]->sname);
 					bputs("\r\1>\r\n");
 					bputs(hdr); 
+					c=bstrlen(hdr);
 				}
-				c=bstrlen(hdr);
 				if(tofile) {
-					sprintf(hdr,"(%u) %s",j+1,cfg.dir[dirnum]->lname);
-					write(tofile,hdr,strlen(hdr)); 
+					c += fprintf(tofile,"(%u) %s",j+1,cfg.dir[dirnum]->lname);
 				}
 				else {
 					sprintf(hdr,text[ShortHdrDir],j+1,cfg.dir[dirnum]->lname);
 					bputs(hdr); 
+					c+=bstrlen(hdr);
 				}
-				c+=bstrlen(hdr);
 				if(tofile) {
-					write(tofile,crlf,2);
-					sprintf(hdr,"%*s",c,nulstr);
-					memset(hdr,0xC4,c);
-					strcat(hdr,crlf);
-					write(tofile,hdr,strlen(hdr)); 
+					fprintf(tofile,"\r\n%.*s\r\n", c, "----------------------------------------------------------------");
 				}
 				else {
 					CRLF;
@@ -313,42 +252,22 @@ int sbbs_t::listfiles(uint dirnum, const char *filespec, int tofile, long mode)
 		next=m;
 		disp=1;
 		if(mode&(FL_EXFIND|FL_VIEW)) {
-			f.dir=dirnum;
-			strcpy(f.name,str);
-			m-=11;
-			f.datoffset=n;
-			f.dateuled=ixbbuf[m+3]|((long)ixbbuf[m+4]<<8)
-				|((long)ixbbuf[m+5]<<16)|((long)ixbbuf[m+6]<<24);
-			f.datedled=ixbbuf[m+7]|((long)ixbbuf[m+8]<<8)
-				|((long)ixbbuf[m+9]<<16)|((long)ixbbuf[m+10]<<24);
-			m+=11;
-			f.size=0;
-			getfiledat(&cfg,&f);
 			if(!found)
 				bputs("\r\1>");
-			if(mode&FL_EXFIND) {
-				if(!viewfile(&f,1)) {
-					free((char *)ixbbuf);
-					free((char *)datbuf);
-					return(-1); } 
+			if(!viewfile(f, INT_TO_BOOL(mode&FL_EXFIND))) {
+				found = -1;
+				break;
 			}
-			else {
-				if(!viewfile(&f,0)) {
-					free((char *)ixbbuf);
-					free((char *)datbuf);
-					return(-1); 
-				} 
-			} 
+			CRLF;
 		}
-
 		else if(tofile)
-			listfiletofile(str,&datbuf[n],dirnum,tofile);
+			listfiletofile(f, tofile);
 		else if(mode&FL_FINDDESC)
-			disp=listfile(str,&datbuf[n],dirnum,filespec,letter,n);
+			disp=listfile(f, dirnum, filespec, letter);
 		else
-			disp=listfile(str,&datbuf[n],dirnum,nulstr,letter,n);
+			disp=listfile(f, dirnum, nulstr, letter);
 		if(!disp && letter>'A') {
-			next=m-F_IXBSIZE;
+			next=m-1;
 			letter--; 
 		}
 		else {
@@ -356,24 +275,17 @@ int sbbs_t::listfiles(uint dirnum, const char *filespec, int tofile, long mode)
 			found++; 
 		}
 		if(sys_status&SS_ABORT) {
-			free((char *)ixbbuf);
-			free((char *)datbuf);
-			return(-1); 
+			found = -1;
+			break;
 		}
 		if(mode&(FL_EXFIND|FL_VIEW))
 			continue;
 		if(useron.misc&BATCHFLAG && !tofile) {
 			if(disp) {
-				strcpy(bf[letter-'A'].name,str);
-				m-=11;
-				bf[letter-'A'].datoffset=n;
-				bf[letter-'A'].dateuled=ixbbuf[m+3]|((long)ixbbuf[m+4]<<8)
-					|((long)ixbbuf[m+5]<<16)|((long)ixbbuf[m+6]<<24);
-				bf[letter-'A'].datedled=ixbbuf[m+7]|((long)ixbbuf[m+8]<<8)
-					|((long)ixbbuf[m+9]<<16)|((long)ixbbuf[m+10]<<24);
+				bf[letter-'A'] = f;
 				file_row[letter-'A'] = currow;
 			}
-			m+=11;
+			m++;
 			if(flagprompt || letter=='Z' || !disp ||
 				(filespec[0] && !strchr(filespec,'*') && !strchr(filespec,'?')
 				&& !(mode&FL_FINDDESC))
@@ -382,31 +294,31 @@ int sbbs_t::listfiles(uint dirnum, const char *filespec, int tofile, long mode)
 				flagprompt=0;
 				lncntr=0;
 				lastbat=found;
-				if((int)(i=batchflagprompt(dirnum,bf,file_row,letter-'A'+1,l/F_IXBSIZE))<1) {
-					free((char *)ixbbuf);
-					free((char *)datbuf);
+				if((int)(i=batchflagprompt(&smb, bf, file_row, letter-'A'+1, file_count))<1) {
 					if((int)i==-1)
-						return(-1);
-					else
-						return(found); 
+						found = -1;
+					break;
 				}
 				if(i==2) {
 					next=anchor;
 					found-=(letter-'A')+1; 
 				}
 				else if(i==3) {
-					if((long)anchor-((letter-'A'+1)*F_IXBSIZE)<0) {
+					if((long)anchor-((letter-'A'+1))<0) {
 						next=0;
 						found=0; 
 					}
 					else {
-						next=anchor-((letter-'A'+1)*F_IXBSIZE);
-						found-=letter-'A'+1; } 
+						next=anchor-((letter-'A'+1));
+						found-=letter-'A'+1; 
+					} 
 				}
 				getnodedat(cfg.node_num,&thisnode,0);
 				nodesync();
-				letter='A';	}
-			else letter++; 
+				letter='A';	
+			}
+			else
+				letter++; 
 		}
 		if(useron.misc&BATCHFLAG && !tofile
 			&& lncntr>=rows-2) {
@@ -419,250 +331,133 @@ int sbbs_t::listfiles(uint dirnum, const char *filespec, int tofile, long mode)
 			break; 
 	}
 
-	free((char *)ixbbuf);
-	free((char *)datbuf);
-	return(found);
+	freefiles(file_list, file_count);
+	smb_close(&smb);
+	return found;
 }
 
 /****************************************************************************/
 /* Prints one file's information on a single line                           */
 /* Return 1 if displayed, 0 otherwise										*/
 /****************************************************************************/
-bool sbbs_t::listfile(const char *fname, const char *buf, uint dirnum
-	, const char *search, const char letter, ulong datoffset)
+bool sbbs_t::listfile(file_t* f, uint dirnum, const char *search, const char letter)
 {
-	char	str[256],ext[513]="",*ptr,*cr,*lf,exist=1;
+	char	*ptr,*cr,*lf;
+	bool	exist = true;
+	char*	ext=NULL;
 	char	path[MAX_PATH+1];
-	char 	tmp[512];
-    uchar	alt;
     int		i,j;
-    ulong	cdt;
-	off_t	size;
+    off_t	cdt;
 	int		size_attr=clr_filecdt;
 
-	if(buf[F_MISC]!=ETX && (buf[F_MISC]-' ')&FM_EXTDESC && useron.misc&EXTDESC) {
-		getextdesc(&cfg,dirnum,datoffset,ext);
-		if(useron.misc&BATCHFLAG && lncntr+extdesclines(ext)>=rows-2 && letter!='A')
-			return(false); 
+	if(f->extdesc != NULL && *f->extdesc && (useron.misc&EXTDESC)) {
+		ext = f->extdesc;
+		if((useron.misc&BATCHFLAG) && lncntr+extdesclines(ext)>=rows-2 && letter!='A')
+			return false;
 	}
 
 	attr(cfg.color[clr_filename]);
-	bputs(fname);
-
-	getrec(buf,F_ALTPATH,2,str);
-	alt=(uchar)ahtoul(str);
-	sprintf(path,"%s%s",alt>0 && alt<=cfg.altpaths ? cfg.altpath[alt-1]:cfg.dir[dirnum]->path
-		,unpadfname(fname,tmp));
+	char fname[13];	/* This is one of the only 8.3 filename formats left! (used for display purposes only) */
+	bprintf("%-*s", (int)sizeof(fname)-1, format_filename(f->name, fname, sizeof(fname)-1, /* pad: */TRUE));
+	getfilepath(&cfg, f, path);
 
-	if(buf[F_MISC]!=ETX && (buf[F_MISC]-' ')&FM_EXTDESC) {
-		if(!(useron.misc&EXTDESC))
-			outchar('+');
-		else
-			outchar(' '); 
-	}
+	if(f->extdesc != NULL && *f->extdesc && !(useron.misc&EXTDESC))
+		outchar('+');
 	else
-		outchar(' ');
+		outchar(' '); 
 	if(useron.misc&BATCHFLAG) {
 		attr(cfg.color[clr_filedesc]);
 		bprintf("%c",letter); 
 	}
-	getrec(buf,F_CDT,LEN_FCDT,str);
-	cdt=atol(str);
-	if(cfg.dir[dirnum]->misc&DIR_FCHK) {
-		if(!fexistcase(path)) {
-			exist=0;
-			size_attr = clr_err; 
-		}
-		else if((cfg.dir[dirnum]->misc&DIR_FREE) && (size=flength(path)) >= 0)
-			cdt = size;
+	cdt = f->cost;
+	if(f->size == -1) {
+		exist = false;
+		size_attr = clr_err; 
 	}
+	else if((cfg.dir[dirnum]->misc & (DIR_FREE | DIR_FCHK)) == (DIR_FREE | DIR_FCHK))
+		cdt = getfilesize(&cfg, f);
+	char bytes[32];
+	unsigned units = 1;
+	do {
+		byte_estimate_to_str(cdt, bytes, sizeof(bytes), units, /* precision: */1);
+		units *= 1024;
+	} while(strlen(bytes) > 6 && units < 1024 * 1024 * 1024);
 	attr(cfg.color[size_attr]);
 	if(useron.misc&BATCHFLAG) {
 		if(!cdt && !(cfg.dir[dirnum]->misc&DIR_FREE)) {
 			attr(curatr^(HIGH|BLINK));
 			bputs("  FREE"); 
 		}
-		else if(cdt>=(1024*1024*1024))
-			bprintf("%5.1fG",cdt/(1024.0*1024.0*1024.0));
-		else if(cdt>=(1024*1024))
-			bprintf("%5.1fM",cdt/(1024.0*1024.0));
-		else if(cdt>=1024)
-			bprintf("%5.1fK",cdt/1024.0); 
-		else
-			bprintf("%5luB", cdt);
+		else 
+			bprintf("%6s", bytes);
 	}
 	else {
 		if(!cdt && !(cfg.dir[dirnum]->misc&DIR_FREE)) {  /* FREE file */
 			attr(curatr^(HIGH|BLINK));
 			bputs("   FREE"); 
 		}
-		else if(cdt>=(1024*1024*1024))
-			bprintf("%6.1fG",cdt/(1024.0*1024.0*1024.0));
-		else if(cdt>=(1024*1024))
-			bprintf("%6.1fM",cdt/(1024.0*1024.0));
-		else if(cdt>=1024)
-			bprintf("%6.1fK",cdt/1024.0); 
-		else
-			bprintf("%6luB", cdt);
+		else 
+			bprintf("%7s", bytes);
 	}
 	if(exist)
 		outchar(' ');
 	else
 		outchar('-');
-	getrec(buf,F_DESC,LEN_FDESC,str);
 	attr(cfg.color[clr_filedesc]);
 
-#ifdef _WIN32
- 
-	if(exist && !(cfg.file_misc&FM_NO_LFN)) {
-		fexistcase(path);	/* Get real (long?) filename */
-		ptr=getfname(path);
-		if(stricmp(ptr,tmp) && stricmp(ptr,str))
-			bprintf("%.*s\r\n%21s",LEN_FDESC,ptr,"");
-	}
-
-#endif
-
-	if(!ext[0]) {
-		if(search[0]) { /* high-light string in string */
-			strcpy(tmp,str);
-			strupr(tmp);
-			ptr=strstr(tmp,search);
-			i=strlen(search);
-			j=ptr-tmp;
-			bprintf("%.*s",j,str);
-			attr(cfg.color[clr_filedesc]^HIGH);
-			bprintf("%.*s",i,str+j);
-			attr(cfg.color[clr_filedesc]);
-			bprintf("%.*s",(int)(strlen(str)-(j+i)),str+j+i); 
+	if(ext == NULL) {
+		char* fdesc = f->desc;
+		SKIP_WHITESPACE(fdesc);
+		if(fdesc == NULL || *fdesc == '\0')
+			bputs(P_TRUNCATE, f->name);
+		else if(search[0]) { /* high-light string in string */
+			ptr = strcasestr(fdesc, search);
+			if(ptr != NULL) {
+				i=strlen(search);
+				j=ptr - fdesc;
+				bprintf("%.*s",j,fdesc);
+				attr(cfg.color[clr_filedesc]^HIGH);
+				bprintf("%.*s",i,fdesc+j);
+				attr(cfg.color[clr_filedesc]);
+				bprintf("%.*s",(int)strlen(fdesc)-(j+i),fdesc+j+i);
+			}
+		}
+		else {
+			bputs(P_TRUNCATE, fdesc);
 		}
-		else
-			bputs(str);
 		CRLF; 
-	}
-	ptr=ext;
-	while(*ptr && ptr<ext+512 && !msgabort()) {
-		cr=strchr(ptr,CR);
-		lf=strchr(ptr,LF);
-		if(lf && (lf<cr || !cr)) cr=lf;
-		if(cr>ptr+LEN_FDESC)
-			cr=ptr+LEN_FDESC;
-		else if(cr)
-			*cr=0;
-		sprintf(str,"%.*s\r\n",LEN_FDESC,ptr);
-		putmsg(str,P_NOATCODES|P_SAVEATR);
-		if(!cr) {
-			if(strlen(ptr)>LEN_FDESC)
+	} else {
+		char* ext_desc = strdup((char*)ext);
+		truncsp(ext_desc);
+		ptr=(char*)ext_desc;
+		SKIP_CRLF(ptr);
+		while(ptr && *ptr && !msgabort()) {
+			cr=strchr(ptr,CR);
+			lf=strchr(ptr,LF);
+			if(lf && (lf<cr || !cr)) cr=lf;
+			if(cr>ptr+LEN_FDESC)
 				cr=ptr+LEN_FDESC;
-			else
-				break; 
+			else if(cr)
+				*cr=0;
+			char str[256];
+			sprintf(str,"%.*s\r\n",LEN_FDESC,ptr);
+			putmsg(str,P_NOATCODES|P_SAVEATR);
+			if(!cr) {
+				if(strlen(ptr)>LEN_FDESC)
+					cr=ptr+LEN_FDESC;
+				else
+					break; 
+			}
+			if(!(*(cr+1)) || !(*(cr+2)))
+				break;
+			bprintf("%21s",nulstr);
+			ptr=cr;
+			if(!(*ptr)) ptr++;
+			while(*ptr==LF || *ptr==CR) ptr++; 
 		}
-		if(!(*(cr+1)) || !(*(cr+2)))
-			break;
-		bprintf("%21s",nulstr);
-		ptr=cr;
-		if(!(*ptr)) ptr++;
-		while(*ptr==LF || *ptr==CR) ptr++; 
-	}
-	return(true);
-}
-
-/****************************************************************************/
-/* Remove credits from uploader of file 'f'                                 */
-/****************************************************************************/
-bool sbbs_t::removefcdt(file_t* f)
-{
-	char	str[128];
-	char 	tmp[512];
-	int		u;
-	long	cdt;
-
-	if((u=matchuser(&cfg,f->uler,TRUE /*sysop_alias*/))==0) {
-	   bputs(text[UnknownUser]);
-	   return(false); 
-	}
-	cdt=0L;
-	if(cfg.dir[f->dir]->misc&DIR_CDTMIN && cur_cps) {
-		if(cfg.dir[f->dir]->misc&DIR_CDTUL)
-			cdt=((ulong)(f->cdt*(cfg.dir[f->dir]->up_pct/100.0))/cur_cps)/60;
-		if(cfg.dir[f->dir]->misc&DIR_CDTDL
-			&& f->timesdled)  /* all downloads */
-			cdt+=((ulong)((long)f->timesdled
-				*f->cdt*(cfg.dir[f->dir]->dn_pct/100.0))/cur_cps)/60;
-		adjustuserrec(&cfg,u,U_MIN,10,-cdt);
-		sprintf(str,"%lu minute",cdt);
-		sprintf(tmp,text[FileRemovedUserMsg]
-			,f->name,cdt ? str : text[No]);
-		putsmsg(&cfg,u,tmp); 
-	}
-	else {
-		if(cfg.dir[f->dir]->misc&DIR_CDTUL)
-			cdt=(ulong)(f->cdt*(cfg.dir[f->dir]->up_pct/100.0));
-		if(cfg.dir[f->dir]->misc&DIR_CDTDL
-			&& f->timesdled)  /* all downloads */
-			cdt+=(ulong)((long)f->timesdled
-				*f->cdt*(cfg.dir[f->dir]->dn_pct/100.0));
-		adjustuserrec(&cfg,u,U_CDT,10,-cdt);
-		sprintf(tmp,text[FileRemovedUserMsg]
-			,f->name,cdt ? ultoac(cdt,str) : text[No]);
-		putsmsg(&cfg,u,tmp); 
-	}
-
-	adjustuserrec(&cfg,u,U_ULB,10,-f->size);
-	adjustuserrec(&cfg,u,U_ULS,5,-1);
-	return(true);
-}
-
-bool sbbs_t::removefile(file_t* f)
-{
-	char str[256];
-
-	if(removefiledat(&cfg,f)) {
-		SAFEPRINTF3(str,"removed %s from %s %s"
-			,f->name
-			,cfg.lib[cfg.dir[f->dir]->lib]->sname,cfg.dir[f->dir]->sname);
-		logline("U-",str);
-		return(true);
-	}
-	SAFEPRINTF2(str,"%s %s",cfg.lib[cfg.dir[f->dir]->lib]->sname,cfg.dir[f->dir]->sname);
-	errormsg(WHERE, ERR_REMOVE, f->name, 0, str);
-	return(false);
-}
-
-/****************************************************************************/
-/* Move file 'f' from f.dir to newdir                                       */
-/****************************************************************************/
-bool sbbs_t::movefile(file_t* f, int newdir)
-{
-	char str[MAX_PATH+1],path[MAX_PATH+1],fname[128],ext[1024];
-	int olddir=f->dir;
-
-	if(findfile(&cfg,newdir,f->name)) {
-		bprintf(text[FileAlreadyThere],f->name);
-		return(false); 
+		free(ext_desc);
 	}
-	getextdesc(&cfg,olddir,f->datoffset,ext);
-	if(cfg.dir[olddir]->misc&DIR_MOVENEW)
-		f->dateuled=time32(NULL);
-	unpadfname(f->name,fname);
-	removefiledat(&cfg,f);
-	f->dir=newdir;
-	addfiledat(&cfg,f);
-	bprintf(text[MovedFile],f->name
-		,cfg.lib[cfg.dir[f->dir]->lib]->sname,cfg.dir[f->dir]->sname);
-	sprintf(str,"moved %s to %s %s",f->name
-		,cfg.lib[cfg.dir[f->dir]->lib]->sname,cfg.dir[f->dir]->sname);
-	logline(nulstr,str);
-	if(!f->altpath) {	/* move actual file */
-		sprintf(str,"%s%s",cfg.dir[olddir]->path,fname);
-		if(fexistcase(str)) {
-			sprintf(path,"%s%s",cfg.dir[f->dir]->path,getfname(str));
-			mv(str,path,0); 
-		} 
-	}
-	if(f->misc&FM_EXTDESC)
-		putextdesc(&cfg,f->dir,f->datoffset,ext);
-	return(true);
+	return true;
 }
 
 /****************************************************************************/
@@ -670,29 +465,28 @@ bool sbbs_t::movefile(file_t* f, int newdir)
 /* Returns -1 if 'Q' or Ctrl-C, 0 if skip, 1 if [Enter], 2 otherwise        */
 /* or 3, backwards. 														*/
 /****************************************************************************/
-int sbbs_t::batchflagprompt(uint dirnum, file_t* bf, ulong* row, uint total
+int sbbs_t::batchflagprompt(smb_t* smb, file_t** bf, ulong* row, uint total
 							,long totalfiles)
 {
-	char	ch,str[256],fname[128],*p,remcdt=0,remfile=0;
+	char	ch,str[256],*p,remcdt=0,remfile=0;
 	int		c, d;
-	char 	tmp[512];
+	char 	path[MAX_PATH + 1];
 	uint	i,j,ml=0,md=0,udir,ulib;
-	file_t	f;
 
 	for(ulib=0;ulib<usrlibs;ulib++)
-		if(usrlib[ulib]==cfg.dir[dirnum]->lib)
+		if(usrlib[ulib]==cfg.dir[smb->dirnum]->lib)
 			break;
 	for(udir=0;udir<usrdirs[ulib];udir++)
-		if(usrdir[ulib][udir]==dirnum)
+		if(usrdir[ulib][udir]==smb->dirnum)
 			break;
 
 	CRLF;
 	while(online) {
 		bprintf(text[BatchFlagPrompt]
 			,ulib+1
-			,cfg.lib[cfg.dir[dirnum]->lib]->sname
+			,cfg.lib[cfg.dir[smb->dirnum]->lib]->sname
 			,udir+1
-			,cfg.dir[dirnum]->sname
+			,cfg.dir[smb->dirnum]->sname
 			,total, totalfiles);
 		ch=getkey(K_UPPER);
 		clearline();
@@ -714,12 +508,7 @@ int sbbs_t::batchflagprompt(uint dirnum, file_t* bf, ulong* row, uint total
 				return(2); 
 			}
 			if(total==1) {
-				f.dir=dirnum;
-				strcpy(f.name,bf[0].name);
-				f.datoffset=bf[0].datoffset;
-				f.size=0;
-				getfiledat(&cfg,&f);
-				addtobatdl(&f);
+				addtobatdl(bf[0]);
 				if(ch=='D')
 					start_batch_download();
 				CRLF;
@@ -730,49 +519,40 @@ int sbbs_t::batchflagprompt(uint dirnum, file_t* bf, ulong* row, uint total
 			for(i=0; i < total; i++)
 				add_hotspot((char)('A' + i), /* hungry: */true, -1, -1, row[i]);
 			bputs(text[BatchDlFlags]);
-			d=getstr(str,BF_MAX,K_UPPER|K_LOWPRIO|K_NOCRLF);
+			d=getstr(str, BF_MAX, K_NOCRLF);
 			clear_hotspots();
 			mouse_hotspots = saved_hotspots;
 			lncntr=0;
 			if(sys_status&SS_ABORT)
 				return(-1);
-			if(d) { 	/* d is string length */
+			if(d > 0) { 	/* d is string length */
+				strupr(str);
 				CRLF;
 				lncntr=0;
 				for(c=0;c<d;c++) {
-					if(batdn_total>=cfg.max_batdn) {
+					if(batdn_total() >= cfg.max_batdn) {
 						bprintf(text[BatchDlQueueIsFull],str+c);
 						break; 
 					}
 					if(str[c]=='*' || strchr(str+c,'.')) {     /* filename or spec given */
-						f.dir=dirnum;
+//						f.dir=dirnum;
 						p=strchr(str+c,' ');
 						if(!p) p=strchr(str+c,',');
 						if(p) *p=0;
 						for(i=0;i<total;i++) {
-							if(batdn_total>=cfg.max_batdn) {
+							if(batdn_total() >= cfg.max_batdn) {
 								bprintf(text[BatchDlQueueIsFull],str+c);
 								break; 
 							}
-							padfname(str+c,tmp);
-							if(filematch(bf[i].name,tmp)) {
-								strcpy(f.name,bf[i].name);
-								f.datoffset=bf[i].datoffset;
-								f.size=0;
-								getfiledat(&cfg,&f);
-								addtobatdl(&f); 
+							if(filematch(bf[i]->name, str+c)) {
+								addtobatdl(bf[i]); 
 							} 
 						} 
 					}
 					if(strchr(str+c,'.'))
 						c+=strlen(str+c);
 					else if(str[c]<'A'+(char)total && str[c]>='A') {
-						f.dir=dirnum;
-						strcpy(f.name,bf[str[c]-'A'].name);
-						f.datoffset=bf[str[c]-'A'].datoffset;
-						f.size=0;
-						getfiledat(&cfg,&f);
-						addtobatdl(&f);
+						addtobatdl(bf[str[c]-'A']);
 					} 
 				}
 				if(ch=='D')
@@ -786,14 +566,7 @@ int sbbs_t::batchflagprompt(uint dirnum, file_t* bf, ulong* row, uint total
 
 		if(ch=='E' || ch=='V') {    /* Extended Info */
 			if(total==1) {
-				f.dir=dirnum;
-				strcpy(f.name,bf[0].name);
-				f.datoffset=bf[0].datoffset;
-				f.dateuled=bf[0].dateuled;
-				f.datedled=bf[0].datedled;
-				f.size=0;
-				getfiledat(&cfg,&f);
-				if(!viewfile(&f,ch=='E'))
+				if(!viewfile(bf[0], ch=='E'))
 					return(-1);
 				return(2); 
 			}
@@ -802,31 +575,25 @@ int sbbs_t::batchflagprompt(uint dirnum, file_t* bf, ulong* row, uint total
 			for(i=0; i < total; i++)
 				add_hotspot((char)('A' + i), /* hungry: */true, -1, -1, row[i]);
 			bputs(text[BatchDlFlags]);
-			d=getstr(str,BF_MAX,K_UPPER|K_LOWPRIO|K_NOCRLF);
+			d=getstr(str, BF_MAX, K_NOCRLF);
 			clear_hotspots();
 			mouse_hotspots = saved_hotspots;
 			lncntr=0;
 			if(sys_status&SS_ABORT)
 				return(-1);
-			if(d) { 	/* d is string length */
+			if(d > 0) { 	/* d is string length */
+				strupr(str);
 				CRLF;
 				lncntr=0;
 				for(c=0;c<d;c++) {
 					if(str[c]=='*' || strchr(str+c,'.')) {     /* filename or spec given */
-						f.dir=dirnum;
+//						f.dir=dirnum;
 						p=strchr(str+c,' ');
 						if(!p) p=strchr(str+c,',');
 						if(p) *p=0;
 						for(i=0;i<total;i++) {
-							padfname(str+c,tmp);
-							if(filematch(bf[i].name,tmp)) {
-								strcpy(f.name,bf[i].name);
-								f.datoffset=bf[i].datoffset;
-								f.dateuled=bf[i].dateuled;
-								f.datedled=bf[i].datedled;
-								f.size=0;
-								getfiledat(&cfg,&f);
-								if(!viewfile(&f,ch=='E'))
+							if(filematch(bf[i]->name, str+c)) {
+								if(!viewfile(bf[i], ch=='E'))
 									return(-1); 
 							} 
 						} 
@@ -834,16 +601,11 @@ int sbbs_t::batchflagprompt(uint dirnum, file_t* bf, ulong* row, uint total
 					if(strchr(str+c,'.'))
 						c+=strlen(str+c);
 					else if(str[c]<'A'+(char)total && str[c]>='A') {
-						f.dir=dirnum;
-						strcpy(f.name,bf[str[c]-'A'].name);
-						f.datoffset=bf[str[c]-'A'].datoffset;
-						f.dateuled=bf[str[c]-'A'].dateuled;
-						f.datedled=bf[str[c]-'A'].datedled;
-						f.size=0;
-						getfiledat(&cfg,&f);
-						if(!viewfile(&f,ch=='E'))
-							return(-1); } 
+						if(!viewfile(bf[str[c]-'A'], ch=='E'))
+							return(-1); 
+					} 
 				}
+				cond_newline();
 				return(2); 
 			}
 			clearline();
@@ -852,7 +614,7 @@ int sbbs_t::batchflagprompt(uint dirnum, file_t* bf, ulong* row, uint total
 
 		if((ch=='R' || ch=='M')     /* Delete or Move */
 			&& !(useron.rest&FLAG('R'))
-			&& (dir_op(dirnum) || useron.exempt&FLAG('R'))) {
+			&& (dir_op(smb->dirnum) || useron.exempt&FLAG('R'))) {
 			if(total==1) {
 				strcpy(str,"A");
 				d=1; 
@@ -863,33 +625,37 @@ int sbbs_t::batchflagprompt(uint dirnum, file_t* bf, ulong* row, uint total
 				for(i=0; i < total; i++)
 					add_hotspot((char)('A' + i), /* hungry: */true, -1, -1, row[i]);
 				bputs(text[BatchDlFlags]);
-				d=getstr(str,BF_MAX,K_UPPER|K_LOWPRIO|K_NOCRLF);
+				d=getstr(str, BF_MAX, K_NOCRLF);
 				clear_hotspots();
 				mouse_hotspots = saved_hotspots;
 			}
 			lncntr=0;
 			if(sys_status&SS_ABORT)
 				return(-1);
-			if(d) { 	/* d is string length */
-				CRLF;
+			if(d > 0) { 	/* d is string length */
+				strupr(str);
+				if(total > 1)
+					newline();
 				if(ch=='R') {
 					if(noyes(text[RemoveFileQ]))
 						return(2);
-					remcdt=remfile=1;
-					if(dir_op(dirnum)) {
+					remcdt = TRUE;
+					remfile = TRUE;
+					if(dir_op(smb->dirnum)) {
 						remcdt=!noyes(text[RemoveCreditsQ]);
-						remfile=!noyes(text[DeleteFileQ]); } 
+						remfile=!noyes(text[DeleteFileQ]);
+					}
 				}
 				else if(ch=='M') {
 					CRLF;
 					for(i=0;i<usrlibs;i++)
 						bprintf(text[MoveToLibLstFmt],i+1,cfg.lib[usrlib[i]]->lname);
 					SYNC;
-					bprintf(text[MoveToLibPrompt],cfg.dir[dirnum]->lib+1);
+					bprintf(text[MoveToLibPrompt],cfg.dir[smb->dirnum]->lib+1);
 					if((int)(ml=getnum(usrlibs))==-1)
 						return(2);
 					if(!ml)
-						ml=cfg.dir[dirnum]->lib;
+						ml=cfg.dir[smb->dirnum]->lib;
 					else
 						ml--;
 					CRLF;
@@ -908,66 +674,44 @@ int sbbs_t::batchflagprompt(uint dirnum, file_t* bf, ulong* row, uint total
 				lncntr=0;
 				for(c=0;c<d;c++) {
 					if(str[c]=='*' || strchr(str+c,'.')) {     /* filename or spec given */
-						f.dir=dirnum;
+//						f.dir=dirnum;
 						p=strchr(str+c,' ');
 						if(!p) p=strchr(str+c,',');
 						if(p) *p=0;
 						for(i=0;i<total;i++) {
-							padfname(str+c,tmp);
-							if(filematch(bf[i].name,tmp)) {
-								strcpy(f.name,bf[i].name);
-								unpadfname(f.name,fname);
-								f.datoffset=bf[i].datoffset;
-								f.dateuled=bf[i].dateuled;
-								f.datedled=bf[i].datedled;
-								f.size=0;
-								getfiledat(&cfg,&f);
-								if(f.opencount) {
-									bprintf(text[FileIsOpen]
-										,f.opencount,f.opencount>1 ? "s":nulstr);
-									continue; 
-								}
+							if(filematch(bf[i]->name, str+c)) {
 								if(ch=='R') {
-									removefile(&f);
-									if(remfile) {
-										sprintf(tmp,"%s%s",cfg.dir[f.dir]->path,fname);
-										remove(tmp); 
+									if(removefile(smb, bf[i])) {
+										if(remfile) {
+											if(remove(getfilepath(&cfg, bf[i], path)) != 0)
+												errormsg(WHERE, ERR_REMOVE, path);
+										}
+										if(remcdt)
+											removefcdt(bf[i]);
 									}
-									if(remcdt)
-										removefcdt(&f); 
 								}
 								else if(ch=='M')
-									movefile(&f,usrdir[ml][md]); 
+									movefile(smb, bf[i], usrdir[ml][md]); 
 							} 
 						} 
 					}
 					if(strchr(str+c,'.'))
 						c+=strlen(str+c);
 					else if(str[c]<'A'+(char)total && str[c]>='A') {
-						f.dir=dirnum;
-						strcpy(f.name,bf[str[c]-'A'].name);
-						unpadfname(f.name,fname);
-						f.datoffset=bf[str[c]-'A'].datoffset;
-						f.dateuled=bf[str[c]-'A'].dateuled;
-						f.datedled=bf[str[c]-'A'].datedled;
-						f.size=0;
-						getfiledat(&cfg,&f);
-						if(f.opencount) {
-							bprintf(text[FileIsOpen]
-								,f.opencount,f.opencount>1 ? "s":nulstr);
-							continue; 
-						}
+						file_t* f = bf[str[c]-'A'];
 						if(ch=='R') {
-							removefile(&f);
-							if(remfile) {
-								sprintf(tmp,"%s%s",cfg.dir[f.dir]->path,fname);
-								remove(tmp); 
+							if(removefile(smb, f)) {
+								if(remfile) {
+									if(remove(getfilepath(&cfg, f, path)) != 0 && fexist(path))
+										errormsg(WHERE, ERR_REMOVE, path);
+								}
+								if(remcdt)
+									removefcdt(f);
 							}
-							if(remcdt)
-								removefcdt(&f); 
 						}
 						else if(ch=='M')
-							movefile(&f,usrdir[ml][md]); } 
+							movefile(smb, f, usrdir[ml][md]); 
+					} 
 				}
 				return(2); 
 			}
@@ -986,79 +730,40 @@ int sbbs_t::batchflagprompt(uint dirnum, file_t* bf, ulong* row, uint total
 /* action depending on 'mode.'                                              */
 /* Returns number of files matching filespec that were found                */
 /****************************************************************************/
-int sbbs_t::listfileinfo(uint dirnum, char *filespec, long mode)
+int sbbs_t::listfileinfo(uint dirnum, const char *filespec, long mode)
 {
-	char	str[258],path[258],dirpath[256],done=0,ch,fname[13],ext[513];
+	char	str[MAX_PATH + 1],path[MAX_PATH + 1],dirpath[MAX_PATH + 1],done=0,ch;
 	char 	tmp[512];
-	uchar	*ixbbuf,*usrxfrbuf=NULL,*p;
-	int		file;
 	int		error;
 	int		found=0;
     uint	i,j;
-	long	usrxfrlen=0;
-    long	m,l;
-	long	usrcdt;
+    size_t	m;
     time_t	start,end,t;
-    file_t	f;
+    file_t*	f;
 	struct	tm tm;
+	smb_t	smb;
 
-	sprintf(str,"%sxfer.ixt",cfg.data_dir);
-	if(mode==FI_USERXFER) {
-		if(flength(str)<1L)
-			return(0);
-		if((file=nopen(str,O_RDONLY))==-1) {
-			errormsg(WHERE,ERR_OPEN,str,O_RDONLY);
-			return(0); 
-		}
-		usrxfrlen=(long)filelength(file);
-		if((usrxfrbuf=(uchar *)malloc(usrxfrlen))==NULL) {
-			close(file);
-			errormsg(WHERE,ERR_ALLOC,str,usrxfrlen);
-			return(0); 
-		}
-		if(read(file,usrxfrbuf,usrxfrlen)!=usrxfrlen) {
-			close(file);
-			free(usrxfrbuf);
-			errormsg(WHERE,ERR_READ,str,usrxfrlen);
-			return(0); 
-		}
-		close(file); 
-	}
-	sprintf(str,"%s%s.ixb",cfg.dir[dirnum]->data_dir,cfg.dir[dirnum]->code);
-	if((file=nopen(str,O_RDONLY))==-1)
-		return(0);
-	l=(long)filelength(file);
-	if(!l) {
-		FREE_AND_NULL(usrxfrbuf);
-		close(file);
-		return(0); 
-	}
-	if((ixbbuf=(uchar *)malloc(l))==NULL) {
-		close(file);
-		FREE_AND_NULL(usrxfrbuf);
-		errormsg(WHERE,ERR_ALLOC,str,l);
-		return(0); 
-	}
-	if(lread(file,ixbbuf,l)!=l) {
-		close(file);
-		errormsg(WHERE,ERR_READ,str,l);
-		free((char *)ixbbuf);
-		if(usrxfrbuf)
-			free(usrxfrbuf);
-		return(0); 
-	}
-	close(file);
-	sprintf(str,"%s%s.dat",cfg.dir[dirnum]->data_dir,cfg.dir[dirnum]->code);
-	if((file=nopen(str,O_RDONLY))==-1) {
-		errormsg(WHERE,ERR_READ,str,O_RDONLY);
-		free((char *)ixbbuf);
-		if(usrxfrbuf)
-			free(usrxfrbuf);
-		return(0); 
+	if(!smb_init_dir(&cfg, &smb, dirnum))
+		return 0;
+	if(smb_open_dir(&cfg, &smb, dirnum) != SMB_SUCCESS)
+		return 0;
+
+	size_t file_count = 0;
+	file_t* file_list = loadfiles(&smb
+		, filespec
+		, /* time_t */0
+		, file_detail_extdesc
+		, (enum file_sort)cfg.dir[dirnum]->sort
+		, &file_count);
+	if(file_list == NULL || file_count < 1) {
+		smb_close(&smb);
+		free(file_list);
+		return 0;
 	}
-	close(file);
+
 	m=0;
-	while(online && !done && m<l) {
+	while(online && !done && m < file_count) {
+		f = &file_list[m];
 		if(mode==FI_REMOVE && dir_op(dirnum))
 			action=NODE_SYSP;
 		else action=NODE_LFIL;
@@ -1066,167 +771,140 @@ int sbbs_t::listfileinfo(uint dirnum, char *filespec, long mode)
 			found=-1;
 			break; 
 		}
-		for(i=0;i<12 && m<l;i++)
-			if(i==8)
-				str[i]=ixbbuf[m]>' ' ? '.' : ' ';
-			else
-				str[i]=ixbbuf[m++];     /* Turns FILENAMEEXT into FILENAME.EXT */
-		str[i]=0;
-		unpadfname(str,fname);
-		if(filespec[0] && !filematch(str,filespec)) {
-			m+=11;
-			continue; 
-		}
-		f.datoffset=ixbbuf[m]|((long)ixbbuf[m+1]<<8)|((long)ixbbuf[m+2]<<16);
-		f.dateuled=ixbbuf[m+3]|((long)ixbbuf[m+4]<<8)
-			|((long)ixbbuf[m+5]<<16)|((long)ixbbuf[m+6]<<24);
-		f.datedled=ixbbuf[m+7]|((long)ixbbuf[m+8]<<8)
-			|((long)ixbbuf[m+9]<<16)|((long)ixbbuf[m+10]<<24);
-		m+=11;
-		if(mode==FI_OLD && f.datedled>ns_time)
+		m++;
+		if(mode==FI_OLD && f->hdr.last_downloaded > ns_time)
 			continue;
-		if((mode==FI_OLDUL || mode==FI_OLD) && f.dateuled>ns_time)
+		if((mode==FI_OLDUL || mode==FI_OLD) && f->hdr.when_written.time > ns_time)
 			continue;
-		f.dir=curdirnum=dirnum;
-		strcpy(f.name,str);
-		f.size=0;
-		getfiledat(&cfg,&f);
-		if(mode==FI_OFFLINE && f.size>=0)
+		curdirnum = dirnum;
+		if(mode==FI_OFFLINE && getfilesize(&cfg, f) >= 0)
 			continue;
-		if(f.altpath>0 && f.altpath<=cfg.altpaths)
-			strcpy(dirpath,cfg.altpath[f.altpath-1]);
-		else
-			strcpy(dirpath,cfg.dir[f.dir]->path);
-		if(mode==FI_CLOSE && !f.opencount)
-			continue;
-		if(mode==FI_USERXFER) {
-			for(p=usrxfrbuf;p<usrxfrbuf+usrxfrlen;p+=24) {
-				sprintf(str,"%17.17s",p);   /* %4.4u %12.12s */
-				if(!strcmp(str+5,f.name) && useron.number==atoi(str))
-					break; 
-			}
-			if(p>=usrxfrbuf+usrxfrlen) /* file wasn't found */
-				continue; 
-		}
-		if((mode==FI_REMOVE) && (!dir_op(dirnum) && stricmp(f.uler
+		SAFECOPY(dirpath, cfg.dir[f->dir]->path);
+		if((mode==FI_REMOVE) && (!dir_op(dirnum) && stricmp(f->from
 			,useron.alias) && !(useron.exempt&FLAG('R'))))
 			continue;
 		found++;
 		if(mode==FI_INFO) {
-			if(!viewfile(&f,1)) {
-				done=1;
-				found=-1; } 
+			switch(viewfile(f, true)) {
+				case 0:
+					done=1;
+					found=-1;
+					break;
+				case -2:
+					m--;
+					if(m)
+						m--;
+					break;
+			} 
 		}
-		else
-			fileinfo(&f);
-		if(mode==FI_CLOSE) {
-			if(!noyes(text[CloseFileRecordQ])) {
-				f.opencount=0;
-				putfiledat(&cfg,&f); } 
+		else {
+			showfileinfo(f, /* show_extdesc: */mode != FI_DOWNLOAD);
+//			newline();
 		}
-		else if(mode==FI_REMOVE || mode==FI_OLD || mode==FI_OLDUL
+		if(mode==FI_REMOVE || mode==FI_OLD || mode==FI_OLDUL
 			|| mode==FI_OFFLINE) {
 			SYNC;
-			CRLF;
-			if(f.opencount) {
-				mnemonics(text[QuitOrNext]);
-				strcpy(str,"QN\r"); 
-			}
-			else if(dir_op(dirnum)) {
+//			CRLF;
+			SAFECOPY(str, "VEQRNP\b-\r");
+			if(dir_op(dirnum)) {
 				mnemonics(text[SysopRemoveFilePrompt]);
-				strcpy(str,"VEFMCQRN\r"); 
+				SAFECAT(str,"FMC");
 			}
 			else if(useron.exempt&FLAG('R')) {
 				mnemonics(text[RExemptRemoveFilePrompt]);
-				strcpy(str,"VEMQRN\r"); 
+				SAFECAT(str,"M");
 			}
-			else {
+			else
 				mnemonics(text[UserRemoveFilePrompt]);
-				strcpy(str,"VEQRN\r"); 
-			}
 			switch(getkeys(str,0)) {
 				case 'V':
-					viewfilecontents(&f);
+					viewfilecontents(f);
 					CRLF;
 					ASYNC;
 					pause();
-					m-=F_IXBSIZE;
+					m--;
 					continue;
 				case 'E':   /* edit file information */
 					if(dir_op(dirnum)) {
 						bputs(text[EditFilename]);
-						strcpy(str,fname);
-						if(!getstr(str,12,K_EDIT|K_AUTODEL))
+						SAFECOPY(str, f->name);
+						if(!getstr(str, MAX_FILENAME_LEN, K_EDIT|K_AUTODEL))
 							break;
-						if(strcmp(str,fname)) { /* rename */
-							padfname(str,path);
-							if(stricmp(str,fname)
-								&& findfile(&cfg,f.dir,path))
+						if(strcmp(str,f->name) != 0) { /* rename */
+							if(stricmp(str,f->name)
+								&& findfile(&cfg, f->dir, path, NULL))
 								bprintf(text[FileAlreadyThere],path);
 							else {
-								SAFEPRINTF2(path,"%s%s",dirpath,fname);
+								SAFEPRINTF2(path,"%s%s",dirpath,f->name);
 								SAFEPRINTF2(tmp,"%s%s",dirpath,str);
 								if(fexistcase(path) && rename(path,tmp))
 									bprintf(text[CouldntRenameFile],path,tmp);
 								else {
 									bprintf(text[FileRenamed],path,tmp);
-									strcpy(fname,str);
-									removefiledat(&cfg,&f);
-									strcpy(f.name,padfname(str,tmp));
-									addfiledat(&cfg,&f); 
+									smb_new_hfield_str(f, SMB_FILENAME, str);
+									updatefile(&cfg, f);
 								} 
 							} 
 						} 
 					}
+					// Description
 					bputs(text[EditDescription]);
-					getstr(f.desc,LEN_FDESC,K_LINE|K_EDIT|K_AUTODEL);
+					char fdesc[LEN_FDESC + 1];
+					SAFECOPY(fdesc, f->desc);
+					getstr(fdesc, sizeof(fdesc)-1, K_LINE|K_EDIT|K_AUTODEL|K_TRIM);
 					if(sys_status&SS_ABORT)
 						break;
-					if(f.misc&FM_EXTDESC) {
+					if(strcmp(fdesc, f->desc))
+						smb_new_hfield_str(f, SMB_FILEDESC, fdesc);
+
+					// Tags
+					if((cfg.dir[dirnum]->misc & DIR_FILETAGS) || dir_op(dirnum)) {
+						char tags[64] = "";
+						bputs(text[TagFilePrompt]);
+						if(f->tags != NULL)
+							SAFECOPY(tags, f->tags);
+						getstr(tags, sizeof(tags)-1, K_LINE|K_EDIT|K_AUTODEL|K_TRIM);
+						if(sys_status&SS_ABORT)
+							break;
+						if((f->tags == NULL && *tags != '\0') || (f->tags != NULL && strcmp(tags, f->tags)))
+							smb_new_hfield_str(f, SMB_TAGS, tags);
+					}
+					// Extended Description
+					if(f->extdesc != NULL && *f->extdesc) {
 						if(!noyes(text[DeleteExtDescriptionQ])) {
-							f.misc&=~FM_EXTDESC; }
+							// TODO
+						} 
 					}
 					if(!dir_op(dirnum)) {
-						putfiledat(&cfg,&f);
+						updatefile(&cfg, f);
 						break; 
 					}
+					char uploader[LEN_ALIAS + 1];
+					SAFECOPY(uploader, f->from);
 					bputs(text[EditUploader]);
-					if(!getstr(f.uler,LEN_ALIAS,K_EDIT|K_AUTODEL))
+					if(!getstr(uploader, sizeof(uploader), K_EDIT|K_AUTODEL))
 						break;
-					ultoa(f.cdt,str,10);
+					smb_new_hfield_str(f, SMB_FILEUPLOADER, uploader);
+					ultoa(f->cost,str,10);
 					bputs(text[EditCreditValue]);
 					getstr(str,10,K_NUMBER|K_EDIT|K_AUTODEL);
 					if(sys_status&SS_ABORT)
 						break;
-					f.cdt=atol(str);
-					ultoa(f.timesdled,str,10);
+					f->cost = atol(str);
+					smb_new_hfield(f, SMB_COST, sizeof(f->cost), &f->cost);
+					ultoa(f->hdr.times_downloaded,str,10);
 					bputs(text[EditTimesDownloaded]);
 					getstr(str,5,K_NUMBER|K_EDIT|K_AUTODEL);
 					if(sys_status&SS_ABORT)
 						break;
-					f.timesdled=atoi(str);
-					if(f.opencount) {
-						ultoa(f.opencount,str,10);
-						bputs(text[EditOpenCount]);
-						getstr(str,3,K_NUMBER|K_EDIT|K_AUTODEL);
-						f.opencount=atoi(str); 
-					}
-					if(cfg.altpaths || f.altpath) {
-						ultoa(f.altpath,str,10);
-						bputs(text[EditAltPath]);
-						getstr(str,3,K_NUMBER|K_EDIT|K_AUTODEL);
-						f.altpath=atoi(str);
-						if(f.altpath>cfg.altpaths)
-							f.altpath=0; 
-					}
+					f->hdr.times_downloaded=atoi(str);
 					if(sys_status&SS_ABORT)
 						break;
-					putfiledat(&cfg,&f);
-					inputnstime32(&f.dateuled);
-					update_uldate(&cfg, &f);
+					inputnstime32((time32_t*)&f->hdr.when_imported.time);
+					updatefile(&cfg, f);
 					break;
 				case 'F':   /* delete file only */
-					SAFEPRINTF2(str,"%s%s",dirpath,fname);
+					SAFEPRINTF2(str,"%s%s",dirpath,f->name);
 					if(!fexistcase(str))
 						bprintf(text[FileDoesNotExist],str);
 					else {
@@ -1234,9 +912,8 @@ int sbbs_t::listfileinfo(uint dirnum, char *filespec, long mode)
 							if(remove(str))
 								bprintf(text[CouldntRemoveFile],str);
 							else {
-								sprintf(tmp,"deleted %s"
-									,str);
-								logline(nulstr,tmp); 
+								SAFEPRINTF(tmp, "deleted %s", str);
+								logline(nulstr, tmp); 
 							} 
 						} 
 					}
@@ -1244,69 +921,40 @@ int sbbs_t::listfileinfo(uint dirnum, char *filespec, long mode)
 				case 'R':   /* remove file from database */
 					if(noyes(text[RemoveFileQ]))
 						break;
-					removefile(&f);
-					SAFEPRINTF2(str,"%s%s",dirpath,fname);
-					if(fexistcase(str)) {
-						if(dir_op(dirnum)) {
-							if(!noyes(text[DeleteFileQ])) {
-								if(remove(str))
-									bprintf(text[CouldntRemoveFile],str);
-								else {
-									sprintf(tmp,"deleted %s"
-										,str);
-									logline(nulstr,tmp); 
+					if(removefile(&smb, f)) {
+						getfilepath(&cfg, f, path);
+						if(fexistcase(path)) {
+							if(dir_op(dirnum)) {
+								if(!noyes(text[DeleteFileQ])) {
+									if(remove(path) != 0)
+										errormsg(WHERE, ERR_REMOVE, path);
+									else {
+										SAFEPRINTF(tmp, "deleted %s", path);
+										logline(nulstr,path); 
+									} 
 								} 
-							} 
+							}
+							else if(remove(str))    /* always remove if not sysop */
+								bprintf(text[CouldntRemoveFile],str);
 						}
-						else if(remove(str))    /* always remove if not sysop */
-							bprintf(text[CouldntRemoveFile],str); 
 					}
 					if(dir_op(dirnum) || useron.exempt&FLAG('R')) {
-						i=cfg.lib[cfg.dir[f.dir]->lib]->offline_dir;
+						i=cfg.lib[cfg.dir[f->dir]->lib]->offline_dir;
 						if(i!=dirnum && i!=INVALID_DIR
-							&& !findfile(&cfg,i,f.name)) {
+							&& !findfile(&cfg, i, f->name, NULL)) {
 							sprintf(str,text[AddToOfflineDirQ]
-								,fname,cfg.lib[cfg.dir[i]->lib]->sname,cfg.dir[i]->sname);
+								,f->name,cfg.lib[cfg.dir[i]->lib]->sname,cfg.dir[i]->sname);
 							if(yesno(str)) {
-								getextdesc(&cfg,f.dir,f.datoffset,ext);
-								f.dir=i;
-								addfiledat(&cfg,&f);
-								if(f.misc&FM_EXTDESC)
-									putextdesc(&cfg,f.dir,f.datoffset,ext); 
+								addfile(&cfg, i, f, f->extdesc);
 							} 
 						} 
 					}
-					if(dir_op(dirnum) || stricmp(f.uler,useron.alias)) {
+					if(dir_op(dirnum) || stricmp(f->from, useron.alias)) {
 						if(noyes(text[RemoveCreditsQ]))
 	/* Fall through */      break; 
 					}
 				case 'C':   /* remove credits only */
-					if((i=matchuser(&cfg,f.uler,TRUE /*sysop_alias*/))==0) {
-						bputs(text[UnknownUser]);
-						break; 
-					}
-					if(dir_op(dirnum)) {
-						usrcdt=(ulong)(f.cdt*(cfg.dir[f.dir]->up_pct/100.0));
-						if(f.timesdled)     /* all downloads */
-							usrcdt+=(ulong)((long)f.timesdled
-								*f.cdt*(cfg.dir[f.dir]->dn_pct/100.0));
-						ultoa(usrcdt,str,10);
-						bputs(text[CreditsToRemove]);
-						getstr(str,10,K_NUMBER|K_LINE|K_EDIT|K_AUTODEL);
-						f.cdt=atol(str); 
-					}
-					usrcdt=adjustuserrec(&cfg,i,U_CDT,10,-(long)f.cdt);
-					if(i==useron.number)
-						useron.cdt=usrcdt;
-					sprintf(str,text[FileRemovedUserMsg]
-						,f.name,f.cdt ? ultoac(f.cdt,tmp) : text[No]);
-					putsmsg(&cfg,i,str);
-					usrcdt=adjustuserrec(&cfg,i,U_ULB,10,-f.size);
-					if(i==useron.number)
-						useron.ulb=usrcdt;
-					usrcdt=adjustuserrec(&cfg,i,U_ULS,5,-1);
-					if(i==useron.number)
-						useron.uls=(ushort)usrcdt;
+					removefcdt(f);
 					break;
 				case 'M':   /* move the file to another dir */
 					CRLF;
@@ -1332,16 +980,24 @@ int sbbs_t::listfileinfo(uint dirnum, char *filespec, long mode)
 						j=usrdirs[i]-1;
 					else j--;
 					CRLF;
-					movefile(&f,usrdir[i][j]);
+					movefile(&smb, f, usrdir[i][j]);
+					break;
+				case 'P':	/* previous */
+				case '-':
+				case '\b':
+					m--;
+					if(m)
+						m--;
 					break;
 				case 'Q':   /* quit */
 					found=-1;
 					done=1;
-					break; } 
+					break; 
+			} 
 		}
-		else if(mode==FI_DOWNLOAD || mode==FI_USERXFER) {
-			SAFEPRINTF2(path,"%s%s",dirpath,fname);
-			if(f.size<1L) { /* getfiledat will set this to -1 if non-existant */
+		else if(mode==FI_DOWNLOAD) {
+			getfilepath(&cfg, f, path);
+			if(getfilesize(&cfg, f) < 1L) { /* getfilesize will set this to -1 if non-existant */
 				SYNC;       /* and 0 byte files shouldn't be d/led */
 				mnemonics(text[QuitOrNext]);
 				if(getkeys("\rQ",0)=='Q') {
@@ -1350,8 +1006,8 @@ int sbbs_t::listfileinfo(uint dirnum, char *filespec, long mode)
 				}
 				continue; 
 			}
-			if(!is_download_free(&cfg,f.dir,&useron,&client)
-				&& f.cdt>(useron.cdt+useron.freecdt)) {
+			if(!is_download_free(&cfg,f->dir,&useron,&client)
+				&& f->cost>(useron.cdt+useron.freecdt)) {
 				SYNC;
 				bprintf(text[YouOnlyHaveNCredits]
 					,ultoac(useron.cdt+useron.freecdt,tmp));
@@ -1362,7 +1018,7 @@ int sbbs_t::listfileinfo(uint dirnum, char *filespec, long mode)
 				}
 				continue; 
 			}
-			if(!chk_ar(cfg.dir[f.dir]->dl_ar,&useron,&client)) {
+			if(!chk_ar(cfg.dir[f->dir]->dl_ar,&useron,&client)) {
 				SYNC;
 				bputs(text[CantDownloadFromDir]);
 				mnemonics(text[QuitOrNext]);
@@ -1372,7 +1028,8 @@ int sbbs_t::listfileinfo(uint dirnum, char *filespec, long mode)
 				}
 				continue; 
 			}
-			if(!(cfg.dir[f.dir]->misc&DIR_TFREE) && f.timetodl>timeleft && !dir_op(dirnum)
+
+			if(!(cfg.dir[f->dir]->misc&DIR_TFREE) && gettimetodl(&cfg, f, cur_cps) > timeleft && !dir_op(dirnum)
 				&& !(useron.exempt&FLAG('T'))) {
 				SYNC;
 				bputs(text[NotEnoughTimeToDl]);
@@ -1384,15 +1041,13 @@ int sbbs_t::listfileinfo(uint dirnum, char *filespec, long mode)
 				continue; 
 			}
 			xfer_prot_menu(XFER_DOWNLOAD);
-			openfile(&f);
 			SYNC;
 			mnemonics(text[ProtocolBatchQuitOrNext]);
 			sprintf(str,"B%cN\r",text[YNQP][2]);
 			for(i=0;i<cfg.total_prots;i++)
 				if(cfg.prot[i]->dlcmd[0]
 					&& chk_ar(cfg.prot[i]->ar,&useron,&client)) {
-					sprintf(tmp,"%c",cfg.prot[i]->mnemonic);
-					strcat(str,tmp); 
+					sprintf(str + strlen(str), "%c", cfg.prot[i]->mnemonic);
 				}
 	//		  ungetkey(useron.prot);
 			ch=(char)getkeys(str,0);
@@ -1401,9 +1056,9 @@ int sbbs_t::listfileinfo(uint dirnum, char *filespec, long mode)
 				done=1; 
 			}
 			else if(ch=='B') {
-				if(!addtobatdl(&f)) {
-					closefile(&f);
-					break; } 
+				if(!addtobatdl(f)) {
+					break; 
+				} 
 			}
 			else if(ch!=CR && ch!='N') {
 				for(i=0;i<cfg.total_prots;i++)
@@ -1411,97 +1066,68 @@ int sbbs_t::listfileinfo(uint dirnum, char *filespec, long mode)
 						&& chk_ar(cfg.prot[i]->ar,&useron,&client))
 						break;
 				if(i<cfg.total_prots) {
-					{
-						delfiles(cfg.temp_dir,ALLFILES);
-						if(cfg.dir[f.dir]->seqdev) {
-							lncntr=0;
-							seqwait(cfg.dir[f.dir]->seqdev);
-							bprintf(text[RetrievingFile],fname);
-							SAFEPRINTF2(str,"%s%s",dirpath,fname);
-							SAFEPRINTF2(path,"%s%s",cfg.temp_dir,fname);
-							mv(str,path,1); /* copy the file to temp dir */
-							if(getnodedat(cfg.node_num,&thisnode,true)==0) {
-								thisnode.aux=0xf0;
-								putnodedat(cfg.node_num,&thisnode);
-							}
-							CRLF; 
+					delfiles(cfg.temp_dir,ALLFILES);
+					if(cfg.dir[f->dir]->seqdev) {
+						lncntr=0;
+						seqwait(cfg.dir[f->dir]->seqdev);
+						bprintf(text[RetrievingFile],f->name);
+						getfilepath(&cfg, f, str);
+						SAFEPRINTF2(path,"%s%s",cfg.temp_dir,f->name);
+						mv(str,path,1); /* copy the file to temp dir */
+						if(getnodedat(cfg.node_num,&thisnode,true)==0) {
+							thisnode.aux=0xf0;
+							putnodedat(cfg.node_num,&thisnode);
 						}
-						for(j=0;j<cfg.total_dlevents;j++)
-							if(!stricmp(cfg.dlevent[j]->ext,f.name+9)
+						CRLF; 
+					}
+					const char* file_ext = getfext(f->name);
+					if(file_ext != NULL) {
+						for(j=0; j<cfg.total_dlevents; j++) {
+							if(!stricmp(cfg.dlevent[j]->ext, file_ext + 1)
 								&& chk_ar(cfg.dlevent[j]->ar,&useron,&client)) {
 								bputs(cfg.dlevent[j]->workstr);
 								external(cmdstr(cfg.dlevent[j]->cmd,path,nulstr,NULL)
 									,EX_OUTL);
 								CRLF; 
 							}
-						getnodedat(cfg.node_num,&thisnode,1);
-						action=NODE_DLNG;
-						t=now+f.timetodl;
-						localtime_r(&t,&tm);
-						thisnode.aux=(tm.tm_hour*60)+tm.tm_min;
-						putnodedat(cfg.node_num,&thisnode); /* calculate ETA */
-						start=time(NULL);
-						error=protocol(cfg.prot[i],XFER_DOWNLOAD,path,nulstr,false);
-						end=time(NULL);
-						if(cfg.dir[f.dir]->misc&DIR_TFREE)
-							starttime+=end-start;
-						if(checkprotresult(cfg.prot[i],error,&f))
-							downloadfile(&f);
-						else
-							notdownloaded(f.size,start,end); 
-						delfiles(cfg.temp_dir,ALLFILES);
-						autohangup(); 
-					} 
+						}
+					}
+					getnodedat(cfg.node_num,&thisnode,1);
+					action=NODE_DLNG;
+					t=now + gettimetodl(&cfg, f, cur_cps);
+					localtime_r(&t,&tm);
+					thisnode.aux=(tm.tm_hour*60)+tm.tm_min;
+					putnodedat(cfg.node_num,&thisnode); /* calculate ETA */
+					start=time(NULL);
+					error=protocol(cfg.prot[i],XFER_DOWNLOAD,path,nulstr,false);
+					end=time(NULL);
+					if(cfg.dir[f->dir]->misc&DIR_TFREE)
+						starttime+=end-start;
+					if(checkprotresult(cfg.prot[i],error, f))
+						downloadedfile(f);
+					else
+						notdownloaded(f->size, start, end); 
+					delfiles(cfg.temp_dir,ALLFILES);
+					autohangup(); 
 				} 
-			}
-			closefile(&f); 
+			} 
 		}
 		if(filespec[0] && !strchr(filespec,'*') && !strchr(filespec,'?')) 
 			break; 
 	}
-	free((char *)ixbbuf);
-	if(usrxfrbuf)
-		free(usrxfrbuf);
+	freefiles(file_list, file_count);
+	smb_close(&smb);
 	return(found);
 }
 
 /****************************************************************************/
-/* Prints one file's information on a single line to a file 'file'          */
+/* Prints one file's information on a single line to a file stream 'fp'		*/
 /****************************************************************************/
-void sbbs_t::listfiletofile(char *fname, char *buf, uint dirnum, int file)
+void sbbs_t::listfiletofile(file_t* f, FILE* fp)
 {
-    char	str[256];
-	char 	tmp[512];
-    uchar	alt;
-    ulong	cdt;
-	bool	exist=true;
-
-	strcpy(str,fname);
-	if(buf[F_MISC]!=ETX && (buf[F_MISC]-' ')&FM_EXTDESC)
-		strcat(str,"+");
-	else
-		strcat(str," ");
-	write(file,str,13);
-	getrec((char *)buf,F_ALTPATH,2,str);
-	alt=(uchar)ahtoul(str);
-	sprintf(str,"%s%s",alt>0 && alt<=cfg.altpaths ? cfg.altpath[alt-1]
-		: cfg.dir[dirnum]->path,unpadfname(fname,tmp));
-	if(cfg.dir[dirnum]->misc&DIR_FCHK && !fexistcase(str))
-		exist=false;
-	getrec((char *)buf,F_CDT,LEN_FCDT,str);
-	cdt=atol(str);
-	if(!cdt)
-		strcpy(str,"   FREE");
-	else
-		sprintf(str,"%7lu",cdt);
-	if(exist)
-		strcat(str," ");
-	else
-		strcat(str,"-");
-	write(file,str,8);
-	getrec((char *)buf,F_DESC,LEN_FDESC,str);
-	write(file,str,strlen(str));
-	write(file,crlf,2);
+	char fname[13];	/* This is one of the only 8.3 filename formats left! (used for display purposes only) */
+	fprintf(fp, "%-*s %10lu %s\r\n", (int)sizeof(fname)-1, format_filename(f->name, fname, sizeof(fname)-1, /* pad: */TRUE)
+		,(ulong)getfilesize(&cfg, f), f->desc);
 }
 
 int extdesclines(char *str)
diff --git a/src/sbbs3/load_cfg.c b/src/sbbs3/load_cfg.c
index 815d0b7f8e88a96fe13eeb9127bf904b5cb66728..a25fe5989ddad305315ca685e908d236d549230f 100644
--- a/src/sbbs3/load_cfg.c
+++ b/src/sbbs3/load_cfg.c
@@ -176,9 +176,6 @@ void prep_cfg(scfg_t* cfg)
 	for(i=0;i<cfg->total_libs;i++) {
 		if(cfg->lib[i]->parent_path[0])
 			prep_dir(cfg->ctrl_dir, cfg->lib[i]->parent_path, sizeof(cfg->lib[i]->parent_path));
-	}
-
-	for(i=0;i<cfg->total_libs;i++) {
 		if((cfg->lib[i]->misc&LIB_DIRS) == 0 || cfg->lib[i]->parent_path[0] == 0)
 			continue;
 		char path[MAX_PATH+1];
@@ -495,3 +492,85 @@ ushort DLLCALL sys_timezone(scfg_t* cfg)
 
 	return(cfg->sys_timezone);
 }
+
+
+int DLLCALL smb_storage_mode(scfg_t* cfg, smb_t* smb)
+{
+	if(smb == NULL || smb->subnum == INVALID_SUB || (smb->status.attr&SMB_EMAIL))
+		return (cfg->sys_misc&SM_FASTMAIL) ? SMB_FASTALLOC : SMB_SELFPACK;
+	if(smb->subnum >= cfg->total_subs)
+		return (smb->status.attr&SMB_HYPERALLOC) ? SMB_HYPERALLOC : SMB_FASTALLOC;
+	if(cfg->sub[smb->subnum]->misc&SUB_HYPER) {
+		smb->status.attr |= SMB_HYPERALLOC;
+		return SMB_HYPERALLOC;
+	}
+	if(cfg->sub[smb->subnum]->misc&SUB_FAST)
+		return SMB_FASTALLOC;
+	return SMB_SELFPACK;
+}
+
+/* Open Synchronet Message Base and create, if necessary (e.g. first time opened) */
+/* If return value is not SMB_SUCCESS, sub-board is not left open */
+int DLLCALL smb_open_sub(scfg_t* cfg, smb_t* smb, unsigned int subnum)
+{
+	int retval;
+	smbstatus_t smb_status = {0};
+
+	if(subnum != INVALID_SUB && subnum >= cfg->total_subs)
+		return SMB_FAILURE;
+	memset(smb, 0, sizeof(smb_t));
+	if(subnum == INVALID_SUB) {
+		SAFEPRINTF(smb->file, "%smail", cfg->data_dir);
+		smb_status.max_crcs	= cfg->mail_maxcrcs;
+		smb_status.max_msgs	= 0;
+		smb_status.max_age	= cfg->mail_maxage;
+		smb_status.attr		= SMB_EMAIL;
+	} else {
+		SAFEPRINTF2(smb->file, "%s%s", cfg->sub[subnum]->data_dir, cfg->sub[subnum]->code);
+		smb_status.max_crcs	= cfg->sub[subnum]->maxcrcs;
+		smb_status.max_msgs	= cfg->sub[subnum]->maxmsgs;
+		smb_status.max_age	= cfg->sub[subnum]->maxage;
+		smb_status.attr		= cfg->sub[subnum]->misc&SUB_HYPER ? SMB_HYPERALLOC :0;
+	}
+	smb->retry_time = cfg->smb_retry_time;
+	if((retval = smb_open(smb)) == SMB_SUCCESS) {
+		if(smb_fgetlength(smb->shd_fp) < sizeof(smbhdr_t) + sizeof(smb->status)) {
+			smb->status = smb_status;
+			if((retval = smb_create(smb)) != SMB_SUCCESS)
+				smb_close(smb);
+		}
+		if(retval == SMB_SUCCESS)
+			smb->subnum = subnum;
+	}
+	return retval;
+}
+
+BOOL DLLCALL smb_init_dir(scfg_t* cfg, smb_t* smb, unsigned int dirnum)
+{
+	if(dirnum >= cfg->total_dirs)
+		return FALSE;
+	memset(smb, 0, sizeof(smb_t));
+	SAFEPRINTF2(smb->file, "%s%s", cfg->dir[dirnum]->data_dir, cfg->dir[dirnum]->code);
+	smb->retry_time = cfg->smb_retry_time;
+	return TRUE;
+}
+
+int DLLCALL smb_open_dir(scfg_t* cfg, smb_t* smb, unsigned int dirnum)
+{
+	int retval;
+
+	if(!smb_init_dir(cfg, smb, dirnum))
+		return SMB_FAILURE;
+	if((retval = smb_open(smb)) != SMB_SUCCESS)
+		return retval;
+	smb->dirnum = dirnum;
+	if(filelength(fileno(smb->shd_fp)) < 1) {
+		smb->status.max_files	= cfg->dir[dirnum]->maxfiles;
+		smb->status.max_age		= cfg->dir[dirnum]->maxage;
+		smb->status.attr		= SMB_FILE_DIRECTORY;
+		if(cfg->dir[dirnum]->misc & DIR_NOHASH)
+			smb->status.attr |= SMB_NOHASH;
+		smb_create(smb);
+	}
+	return SMB_SUCCESS;
+}
diff --git a/src/sbbs3/load_cfg.h b/src/sbbs3/load_cfg.h
index 2523d282e2c926bc837d5d8ff27731f4557d2a6c..63e256a123414de4935be92f0aded046a1be8ff5 100644
--- a/src/sbbs3/load_cfg.h
+++ b/src/sbbs3/load_cfg.h
@@ -23,6 +23,7 @@
 #define _LOAD_CFG_H_
 
 #include "scfgdefs.h"	// scfg_t
+#include "smblib.h"
 #include "dllexport.h"
 
 #ifdef __cplusplus
@@ -36,6 +37,10 @@ DLLEXPORT ushort	sys_timezone(scfg_t* cfg);
 DLLEXPORT char *	prep_dir(const char* base, char* dir, size_t buflen);
 DLLEXPORT char *	prep_code(char *str, const char* prefix);
 DLLEXPORT int 		md(const char *path);
+DLLEXPORT int		smb_storage_mode(scfg_t*, smb_t*);
+DLLEXPORT int		smb_open_sub(scfg_t*, smb_t*, unsigned int subnum);
+DLLEXPORT BOOL		smb_init_dir(scfg_t*, smb_t*, unsigned int dirnum);
+DLLEXPORT int		smb_open_dir(scfg_t*, smb_t*, unsigned int dirnum);
 
 #ifdef __cplusplus
 }
diff --git a/src/sbbs3/logfile.cpp b/src/sbbs3/logfile.cpp
index b4d92520aa6afa52df11fc769de527e8ed16486d..a81dd245d7fa47036ceaab5ea778d3225e3a1f6a 100644
--- a/src/sbbs3/logfile.cpp
+++ b/src/sbbs3/logfile.cpp
@@ -142,7 +142,7 @@ void sbbs_t::logentry(const char *code, const char *entry)
 /****************************************************************************/
 /* Writes 'str' verbatim into node.log 										*/
 /****************************************************************************/
-void sbbs_t::log(char *str)
+void sbbs_t::log(const char *str)
 {
 	if(logfile_fp==NULL || online==ON_LOCAL) return;
 	if(logcol>=78 || (78-logcol)<strlen(str)) {
diff --git a/src/sbbs3/logon.cpp b/src/sbbs3/logon.cpp
index dfd05044452e26be794b6a625e005512fbee10c8..e36827d651f40df1bcee0d6ed43bf8c909baf298 100644
--- a/src/sbbs3/logon.cpp
+++ b/src/sbbs3/logon.cpp
@@ -21,6 +21,7 @@
 
 #include "sbbs.h"
 #include "cmdshell.h"
+#include "filedat.h"
 
 extern "C" void client_on(SOCKET sock, client_t* client, BOOL update);
 
@@ -217,8 +218,46 @@ bool sbbs_t::logon()
 	useron.ltoday++;
 
 	gettimeleft();
-	safe_snprintf(str, sizeof(str), "%sfile/%04u.dwn",cfg.data_dir,useron.number);
-	batch_add_list(str);
+
+	/* Inform the user of what's in their batch upload queue */
+	{
+		str_list_t ini = batch_list_read(&cfg, useron.number, XFER_BATCH_UPLOAD);
+		str_list_t filenames = iniGetSectionList(ini, NULL);
+		for(size_t i = 0; filenames[i] != NULL; i++) {
+			const char* filename = filenames[i];
+			file_t f = {{}};
+			if(batch_file_get(&cfg, ini, filename, &f)) {
+				bprintf(text[FileAddedToUlQueue], f.name, i + 1, cfg.max_batup);
+				smb_freefilemem(&f);
+			} else
+				batch_file_remove(&cfg, useron.number, XFER_BATCH_UPLOAD, filename);
+		}
+		iniFreeStringList(ini);
+		iniFreeStringList(filenames);
+	}
+
+	/* Inform the user of what's in their batch download queue */
+	{
+		str_list_t ini = batch_list_read(&cfg, useron.number, XFER_BATCH_DOWNLOAD);
+		str_list_t filenames = iniGetSectionList(ini, NULL);
+		for(size_t i = 0; filenames[i] != NULL; i++) {
+			const char* filename = filenames[i];
+			file_t f = {{}};
+			if(batch_file_load(&cfg, ini, filename, &f)) {
+				char tmp2[256];
+				getfilesize(&cfg, &f);
+				bprintf(text[FileAddedToBatDlQueue]
+					,f.name, i + 1, cfg.max_batdn
+					,ultoac((ulong)f.cost,tmp)
+					,ultoac((ulong)f.size,tmp2)
+					,sectostr((ulong)f.size / (ulong)cur_cps,str));
+				smb_freefilemem(&f);
+			} else
+				batch_file_remove(&cfg, useron.number, XFER_BATCH_DOWNLOAD, filename);
+		}
+		iniFreeStringList(ini);
+		iniFreeStringList(filenames);
+	}
 	if(!(sys_status&SS_QWKLOGON)) { 	 /* QWK Nodes don't go through this */
 
 		if(cfg.sys_pwdays && useron.pass[0]
@@ -511,10 +550,6 @@ bool sbbs_t::logon()
 
 	if(criterrs && SYSOP)
 		bprintf(text[CriticalErrors],criterrs);
-	if((i=getuserxfers(0,useron.number,0))!=0)
-		bprintf(text[UserXferForYou],i,i>1 ? "s" : nulstr); 
-	if((i=getuserxfers(useron.number,0,0))!=0)
-		bprintf(text[UnreceivedUserXfer],i,i>1 ? "s" : nulstr);
 	SYNC;
 	sys_status&=~SS_PAUSEON;	/* Turn off the pause override flag */
 	if(online==ON_REMOTE)
diff --git a/src/sbbs3/logout.cpp b/src/sbbs3/logout.cpp
index 0d3003febf83ed6dc894fb597c86958cb40f6b6e..01861f09099f272c9d8007385ec29f12cb9ead72 100644
--- a/src/sbbs3/logout.cpp
+++ b/src/sbbs3/logout.cpp
@@ -57,11 +57,9 @@ void sbbs_t::logout()
 
 	if(useron.rest&FLAG('G')) {
 		putuserrec(&cfg,useron.number,U_NAME,LEN_NAME,nulstr);		
-		batdn_total=0; 
+		clearbatdl();
 	}
 
-	batch_create_list();
-
 	if(sys_status&SS_USERON && thisnode.status!=NODE_QUIET && !(useron.rest&FLAG('Q')))
 		for(i=1;i<=cfg.sys_nodes;i++)
 			if(i!=cfg.node_num) {
@@ -86,7 +84,6 @@ void sbbs_t::logout()
 		lprintf(LOG_DEBUG, "executing logout module: %s", cfg.logout_mod);
 		exec_bin(cfg.logout_mod,&main_csi);
 	}
-	backout();
 	SAFEPRINTF2(path,"%smsgs/%4.4u.msg",cfg.data_dir,useron.number);
 	if(fexistcase(path) && !flength(path))		/* remove any 0 byte message files */
 		remove(path);
@@ -154,60 +151,6 @@ void sbbs_t::logout()
 	lprintf(LOG_DEBUG, "logout completed");
 }
 
-/****************************************************************************/
-/* Backout of transactions and statuses for this node 						*/
-/****************************************************************************/
-void sbbs_t::backout()
-{
-	char path[MAX_PATH+1],code[128],*buf;
-	int i,file;
-	long length,l;
-	file_t f;
-
-	SAFEPRINTF(path,"%sbackout.dab",cfg.node_dir);
-	if(flength(path)<1L) {
-		remove(path);
-		return; 
-	}
-	if((file=nopen(path,O_RDONLY))==-1) {
-		errormsg(WHERE,ERR_OPEN,path,O_RDONLY);
-		return; 
-	}
-	length=(long)filelength(file);
-	if((buf=(char *)malloc(length))==NULL) {
-		close(file);
-		errormsg(WHERE,ERR_ALLOC,path,length);
-		return; 
-	}
-	if(read(file,buf,length)!=length) {
-		close(file);
-		free(buf);
-		errormsg(WHERE,ERR_READ,path,length);
-		return; 
-	}
-	close(file);
-	for(l=0;l<length;l+=BO_LEN) {
-		switch(buf[l]) {
-			case BO_OPENFILE:	/* file left open */
-				memcpy(code,buf+l+1,8);
-				code[8]=0;
-				for(i=0;i<cfg.total_dirs;i++)			/* search by code */
-					if(!stricmp(cfg.dir[i]->code,code))
-						break;
-				if(i<cfg.total_dirs) {		/* found internal code */
-					f.dir=i;
-					memcpy(&f.datoffset,buf+l+9,4);
-					closefile(&f); 
-				}
-				break;
-			default:
-				errormsg(WHERE,ERR_CHK,path,buf[l]); 
-		} 
-	}
-	free(buf);
-	remove(path);	/* always remove the backout file */
-}
-
 /****************************************************************************/
 /* Detailed usage stats for each logon                                      */
 /****************************************************************************/
diff --git a/src/sbbs3/mailsrvr.c b/src/sbbs3/mailsrvr.c
index 17295323c8a22759d4fde304ac6ab7630365d1f0..3e28e00cc7179866f0679f1ab7f8c4e31017c16c 100644
--- a/src/sbbs3/mailsrvr.c
+++ b/src/sbbs3/mailsrvr.c
@@ -45,7 +45,8 @@
 #include "multisock.h"
 #include "ssl.h"
 #include "cryptlib.h"
-#include "ver.h"
+#include "git_branch.h"
+#include "git_hash.h"
 
 /* Constants */
 static const char*	server_name="Synchronet Mail Server";
@@ -1296,7 +1297,7 @@ static void pop3_thread(void* arg)
 			strlwr(user.pass);	/* this is case-sensitive, so convert to lowercase */
 			strcat(challenge,user.pass);
 			MD5_calc(digest,challenge,strlen(challenge));
-			MD5_hex((BYTE*)str,digest);
+			MD5_hex(str,digest);
 			if(strcmp(str,response)) {
 				lprintf(LOG_NOTICE,"%04d %s [%s] !FAILED APOP authentication: %s"
 					,socket, client.protocol, host_ip, username);
@@ -2837,7 +2838,7 @@ static void smtp_thread(void* arg)
 	ulong		lines=0;
 	ulong		hdr_lines=0;
 	ulong		hdr_len=0;
-	ulong		length;
+	off_t		length;
 	ulong		badcmds=0;
 	ulong		login_attempts;
 	ulong		waiting;
@@ -3252,14 +3253,14 @@ static void smtp_thread(void* arg)
 					else
 						safe_snprintf(str,sizeof(str),"%s%s%s",head,sender_addr,tail);
 
-					if((telegram_buf=(char*)malloc(length+strlen(str)+1))==NULL) {
+					if((telegram_buf=(char*)malloc((size_t)(length+strlen(str)+1)))==NULL) {
 						lprintf(LOG_CRIT,"%04d %s %s !ERROR allocating %lu bytes of memory for telegram from %s"
 							,socket, client.protocol, client_id, length+strlen(str)+1,sender_addr);
 						sockprintf(socket,client.protocol,session, insuf_stor);
 						continue; 
 					}
 					strcpy(telegram_buf,str);	/* can't use SAFECOPY here */
-					if(fread(telegram_buf+strlen(str),1,length,msgtxt)!=length) {
+					if(fread(telegram_buf+strlen(str),1,(size_t)length,msgtxt)!=length) {
 						lprintf(LOG_ERR,"%04d %s %s !ERROR reading %lu bytes from telegram file"
 							,socket, client.protocol, client_id, length);
 						sockprintf(socket,client.protocol,session, insuf_stor);
@@ -3689,14 +3690,14 @@ static void smtp_thread(void* arg)
 					continue;
 				}
 
-				if((msgbuf=(char*)malloc(length+1))==NULL) {
+				if((msgbuf=(char*)malloc((size_t)(length+1)))==NULL) {
 					lprintf(LOG_CRIT,"%04d %s %s !ERROR allocating %lu bytes of memory"
 						,socket, client.protocol, client_id, length+1);
 					sockprintf(socket,client.protocol,session, insuf_stor);
 					subnum=INVALID_SUB;
 					continue;
 				}
-				fread(msgbuf,length,1,msgtxt);
+				fread(msgbuf,(size_t)length,1,msgtxt);
 				msgbuf[length]=0;	/* ASCIIZ */
 
 				/* Do external JavaScript processing here? */
@@ -3757,7 +3758,7 @@ static void smtp_thread(void* arg)
 						for(i=0;hashes[i];i++)
 							lprintf(LOG_DEBUG,"%04d %s %s Message %s crc32=%x flags=%x length=%u"
 								,socket, client.protocol, client_id, smb_hashsourcetype(hashes[i]->source)
-								,hashes[i]->crc32, hashes[i]->flags, hashes[i]->length);
+								,hashes[i]->data.crc32, hashes[i]->flags, hashes[i]->length);
 
 						lprintf(LOG_DEBUG, "%04d %s %s Searching SPAM database for a match", socket, client.protocol, client_id);
 						if((i=smb_findhash(&spam, hashes, &found, sources, /* Mark: */TRUE))==SMB_SUCCESS) {
@@ -4270,7 +4271,7 @@ static void smtp_thread(void* arg)
 				md5_data[i]=secret[i]^0x5c;	/* opad */
 			memcpy(md5_data+i,digest,sizeof(digest));
 			MD5_calc(digest,md5_data,sizeof(secret)+sizeof(digest));
-			MD5_hex((BYTE*)str,digest);
+			MD5_hex(str,digest);
 			if(strcmp(p,str)) {
 				lprintf(LOG_WARNING,"%04d SMTP %s !%s FAILED CRAM-MD5 authentication"
 					,socket, client_id, relay_user.alias);
@@ -5721,8 +5722,7 @@ static void sendmail_thread(void* arg)
 								md5_data[i]=secret[i]^0x5c;	/* opad */
 							memcpy(md5_data+i,digest,sizeof(digest));
 							MD5_calc(digest,md5_data,sizeof(secret)+sizeof(digest));
-							
-							safe_snprintf(buf,sizeof(buf),"%s %s",startup->relay_user,MD5_hex((BYTE*)str,digest));
+							safe_snprintf(buf,sizeof(buf),"%s %s",startup->relay_user,MD5_hex(str,digest));
 							b64_encode(p=resp,sizeof(resp),buf,strlen(buf));
 							break;
 						default:
@@ -5956,7 +5956,7 @@ const char* DLLCALL mail_ver(void)
 #else
 		,""
 #endif
-		,git_branch, git_hash
+		,GIT_BRANCH, GIT_HASH
 		,__DATE__, __TIME__, compiler
 		);
 
@@ -6061,7 +6061,7 @@ void DLLCALL mail_server(void* arg)
 
 		DESCRIBE_COMPILER(compiler);
 
-		lprintf(LOG_INFO,"Compiled %s/%s %s %s with %s", git_branch, git_hash, __DATE__, __TIME__, compiler);
+		lprintf(LOG_INFO,"Compiled %s/%s %s %s with %s", GIT_BRANCH, GIT_HASH, __DATE__, __TIME__, compiler);
 
 		sbbs_srand();
 
diff --git a/src/sbbs3/mailsrvr.vcxproj b/src/sbbs3/mailsrvr.vcxproj
index 76235c4c64b250c992d461c3d2cb08a21f1ec27a..c4de8b7065f48a6f581ca237857fab69464fabc8 100644
--- a/src/sbbs3/mailsrvr.vcxproj
+++ b/src/sbbs3/mailsrvr.vcxproj
@@ -191,7 +191,6 @@
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
     </ClCompile>
     <ClCompile Include="nopen.c" />
-    <ClCompile Include="ver.cpp" />
   </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\xpdev\xpdev_mt.vcxproj">
diff --git a/src/sbbs3/main.cpp b/src/sbbs3/main.cpp
index ed2d051a45ff276bf89875fe4f21becff2dc0383..fec739c8fc88cbc49c77239d5a6431127edda07c 100644
--- a/src/sbbs3/main.cpp
+++ b/src/sbbs3/main.cpp
@@ -105,6 +105,12 @@ static	link_list_t current_connections;
 int	thread_suid_broken=TRUE;			/* NPTL is no longer broken */
 #endif
 
+/* convenient space-saving global variables */
+extern "C" {
+const char* crlf="\r\n";
+const char* nulstr="";
+};
+
 #define GCES(status, node, sess, action) do {                          \
 	char *GCES_estr;                                                    \
 	int GCES_level;                                                      \
@@ -429,6 +435,10 @@ void* DLLCALL js_GetClassPrivate(JSContext *cx, JSObject *obj, JSClass* cls)
 {
 	void *ret = JS_GetInstancePrivate(cx, obj, cls, NULL);
 
+	/*
+	 * NOTE: Any changes here should also be added to the same function in jsdoor.c
+	 *       (ie: anything not Synchronet specific).
+	 */
 	if(ret == NULL)
 		JS_ReportError(cx, "'%s' instance: No Private Data or Class Mismatch"
 			, cls == NULL ? "???" : cls->name);
@@ -692,6 +702,10 @@ DLLCALL js_DefineSyncProperties(JSContext *cx, JSObject *obj, jsSyncPropertySpec
 {
 	uint i;
 
+	/*
+	 * NOTE: Any changes here should also be added to the same function in jsdoor.c
+	 *       (ie: anything not Synchronet specific).
+	 */
 	for(i=0;props[i].name;i++) {
 		if (props[i].tinyid < 256 && props[i].tinyid > -129) {
 			if(!JS_DefinePropertyWithTinyId(cx, obj,
@@ -713,6 +727,10 @@ DLLCALL js_DefineSyncMethods(JSContext* cx, JSObject* obj, jsSyncMethodSpec *fun
 {
 	uint i;
 
+	/*
+	 * NOTE: Any changes here should also be added to the same function in jsdoor.c
+	 *       (ie: anything not Synchronet specific).
+	 */
 	for(i=0;funcs[i].name;i++)
 		if(!JS_DefineFunction(cx, obj, funcs[i].name, funcs[i].call, funcs[i].nargs, 0))
 			return(JS_FALSE);
@@ -725,6 +743,10 @@ DLLCALL js_SyncResolve(JSContext* cx, JSObject* obj, char *name, jsSyncPropertyS
 	uint i;
 	jsval	val;
 
+	/*
+	 * NOTE: Any changes here should also be added to the same function in jsdoor.c
+	 *       (ie: anything not Synchronet specific).
+	 */
 	if(props) {
 		for(i=0;props[i].name;i++) {
 			if(name==NULL || strcmp(name, props[i].name)==0) {
@@ -1410,10 +1432,16 @@ extern "C" BOOL DLLCALL js_CreateCommonObjects(JSContext* js_cx
 		node_cfg=cfg;
 
 	/* Global Object */
-	if(!js_CreateGlobalObject(js_cx, cfg, methods, js_startup, glob))
+	if(!js_CreateGlobalObject(js_cx, node_cfg, methods, js_startup, glob))
 		return(FALSE);
 
 	do {
+		/*
+		 * NOTE: Where applicable, anything added here should also be added to
+		 *       the same function in jsdoor.c (ie: anything not Synchronet
+		 *       specific).
+		 */
+
 		/* System Object */
 		if(js_CreateSystemObject(js_cx, *glob, node_cfg, uptime, host_name, socklib_desc)==NULL)
 			break;
@@ -1445,10 +1473,18 @@ extern "C" BOOL DLLCALL js_CreateCommonObjects(JSContext* js_cx
 		if(js_CreateMsgBaseClass(js_cx, *glob, cfg)==NULL)
 			break;
 
+		/* FileBase Class */
+		if(js_CreateFileBaseClass(js_cx, *glob, node_cfg)==NULL)
+			break;
+
 		/* File Class */
 		if(js_CreateFileClass(js_cx, *glob)==NULL)
 			break;
 
+		/* Archive Class */
+		if(js_CreateArchiveClass(js_cx, *glob)==NULL)
+			break;
+
 		/* User class */
 		if(js_CreateUserClass(js_cx, *glob, cfg)==NULL)
 			break;
@@ -2609,8 +2645,6 @@ void event_thread(void* arg)
 					sbbs->getusrsubs();
 					bool success = sbbs->unpack_rep(g.gl_pathv[i]);
 					sbbs->delfiles(sbbs->cfg.temp_dir,ALLFILES);		/* clean-up temp_dir after unpacking */
-					sbbs->batch_create_list();	/* FREQs? */
-					sbbs->batdn_total=0;
 					sbbs->online=FALSE;
 					sbbs->console&=~CON_L_ECHO;
 
@@ -2664,11 +2698,8 @@ void event_thread(void* arg)
 					sbbs->console|=CON_L_ECHO;
 					sbbs->getmsgptrs();
 					sbbs->getusrsubs();
-					sbbs->batdn_total=0;
 
 					sbbs->last_ns_time=sbbs->ns_time=sbbs->useron.ns_time;
-					SAFEPRINTF2(bat_list,"%sfile/%04u.dwn",sbbs->cfg.data_dir,sbbs->useron.number);
-					sbbs->batch_add_list(bat_list);
 
 					SAFEPRINTF3(str,"%sfile%c%04u.qwk"
 						,sbbs->cfg.data_dir,PATH_DELIM,sbbs->useron.number);
@@ -2723,7 +2754,6 @@ void event_thread(void* arg)
 						sbbs->console|=CON_L_ECHO;
 						sbbs->getmsgptrs();
 						sbbs->getusrsubs();
-						sbbs->batdn_total=0;
 						SAFEPRINTF3(str,"%sfile%c%04u.qwk"
 							,sbbs->cfg.data_dir,PATH_DELIM,sbbs->useron.number);
 						if(sbbs->pack_qwk(str,&l,true /* pre-pack */)) {
@@ -2878,7 +2908,7 @@ void event_thread(void* arg)
 					|| (sbbs->cfg.qhub[i]->time
 						&& (now_tm.tm_hour*60)+now_tm.tm_min>=sbbs->cfg.qhub[i]->time
 						&& (now_tm.tm_mday!=tm.tm_mday || now_tm.tm_mon!=tm.tm_mon)))
-					&& sbbs->cfg.qhub[i]->days&(1<<now_tm.tm_wday))) {
+							&& sbbs->cfg.qhub[i]->days&(1<<now_tm.tm_wday))) {
 				SAFEPRINTF2(str,"%sqnet/%s.now"
 					,sbbs->cfg.data_dir,sbbs->cfg.qhub[i]->id);
 				if(fexistcase(str)) {
@@ -3418,19 +3448,6 @@ sbbs_t::sbbs_t(ushort node_num, union xp_sockaddr *addr, size_t addr_len, const
 	usrdir=NULL;
 	usrlib_total=0;
 
-	batup_desc=NULL;
-	batup_name=NULL;
-	batup_misc=NULL;
-	batup_dir=NULL;
-	batup_alt=NULL;
-
-	batdn_name=NULL;
-	batdn_dir=NULL;
-	batdn_offset=NULL;
-	batdn_size=NULL;
-	batdn_alt=NULL;
-	batdn_cdt=NULL;
-
 	/* used by update_qwkroute(): */
 	qwknode=NULL;
 	total_qwknodes=0;
@@ -3454,7 +3471,6 @@ sbbs_t::sbbs_t(ushort node_num, union xp_sockaddr *addr, size_t addr_len, const
     usrgrps = 0;
     usrlibs = 0;
     comspec = 0;
-    altul = 0;
     noaccess_str = 0;
     noaccess_val = 0;
     cur_output_rate = output_rate_unlimited;
@@ -3607,9 +3623,6 @@ bool sbbs_t::init()
 		putnodedat(cfg.node_num,&thisnode);
 	}
 
-/** Put in if(cfg.node_num) ? (not needed for server and event threads) */
-	backout();
-
 	/* Reset COMMAND SHELL */
 
 	main_csi.str=(char *)malloc(1024);
@@ -3704,73 +3717,6 @@ bool sbbs_t::init()
 			}
 	}
 
-	if(cfg.max_batup) {
-
-		if((batup_desc=(char **)malloc(sizeof(char *)*cfg.max_batup))==NULL) {
-			errormsg(WHERE, ERR_ALLOC, "batup_desc", sizeof(char *)*cfg.max_batup);
-			return(false);
-		}
-		if((batup_name=(char **)malloc(sizeof(char *)*cfg.max_batup))==NULL) {
-			errormsg(WHERE, ERR_ALLOC, "batup_name", sizeof(char *)*cfg.max_batup);
-			return(false);
-		}
-		if((batup_misc=(long *)malloc(sizeof(long)*cfg.max_batup))==NULL) {
-			errormsg(WHERE, ERR_ALLOC, "batup_misc", sizeof(char *)*cfg.max_batup);
-			return(false);
-		}
-		if((batup_dir=(uint *)malloc(sizeof(uint)*cfg.max_batup))==NULL) {
-			errormsg(WHERE, ERR_ALLOC, "batup_dir", sizeof(char *)*cfg.max_batup);
-			return(false);
-		}
-		if((batup_alt=(ushort *)malloc(sizeof(ushort)*cfg.max_batup))==NULL) {
-			errormsg(WHERE, ERR_ALLOC, "batup_alt", sizeof(char *)*cfg.max_batup);
-			return(false);
-		}
-		for(i=0;i<cfg.max_batup;i++) {
-			if((batup_desc[i]=(char *)malloc(LEN_FDESC+1))==NULL) {
-				errormsg(WHERE, ERR_ALLOC, "batup_desc[x]", LEN_FDESC+1);
-				return(false);
-			}
-			if((batup_name[i]=(char *)malloc(13))==NULL) {
-				errormsg(WHERE, ERR_ALLOC, "batup_name[x]", 13);
-				return(false);
-			}
-		}
-	}
-
-	if(cfg.max_batdn) {
-
-		if((batdn_name=(char **)malloc(sizeof(char *)*cfg.max_batdn))==NULL) {
-			errormsg(WHERE, ERR_ALLOC, "batdn_name", sizeof(char *)*cfg.max_batdn);
-			return(false);
-		}
-		if((batdn_dir=(uint *)malloc(sizeof(uint)*cfg.max_batdn))==NULL)  {
-			errormsg(WHERE, ERR_ALLOC, "batdn_dir", sizeof(uint)*cfg.max_batdn);
-			return(false);
-		}
-		if((batdn_offset=(long *)malloc(sizeof(long)*cfg.max_batdn))==NULL)  {
-			errormsg(WHERE, ERR_ALLOC, "batdn_offset", sizeof(long)*cfg.max_batdn);
-			return(false);
-		}
-		if((batdn_size=(ulong *)malloc(sizeof(ulong)*cfg.max_batdn))==NULL) {
-			errormsg(WHERE, ERR_ALLOC, "batdn_size", sizeof(ulong)*cfg.max_batdn);
-			return(false);
-		}
-		if((batdn_cdt=(ulong *)malloc(sizeof(ulong)*cfg.max_batdn))==NULL) {
-			errormsg(WHERE, ERR_ALLOC, "batdn_cdt", sizeof(long)*cfg.max_batdn);
-			return(false);
-		}
-		if((batdn_alt=(ushort *)malloc(sizeof(ushort)*cfg.max_batdn))==NULL) {
-			errormsg(WHERE, ERR_ALLOC, "batdn_alt", sizeof(ushort)*cfg.max_batdn);
-			return(false);
-		}
-		for(i=0;i<cfg.max_batdn;i++)
-			if((batdn_name[i]=(char *)malloc(13))==NULL) {
-				errormsg(WHERE, ERR_ALLOC, "batdn_name[x]", 13);
-				return(false);
-			}
-	}
-
 #ifdef USE_CRYPTLIB
 	pthread_mutex_init(&ssh_mutex,NULL);
 	ssh_mutex_created = true;
@@ -3882,29 +3828,6 @@ sbbs_t::~sbbs_t()
 	FREE_AND_NULL(usrdirs);
 	FREE_AND_NULL(usrdir);
 
-	/* Batch upload vars */
-	for(i=0;i<cfg.max_batup && batup_desc!=NULL && batup_name!=NULL;i++) {
-		FREE_AND_NULL(batup_desc[i]);
-		FREE_AND_NULL(batup_name[i]);
-	}
-
-	FREE_AND_NULL(batup_desc);
-	FREE_AND_NULL(batup_name);
-	FREE_AND_NULL(batup_misc);
-	FREE_AND_NULL(batup_dir);
-	FREE_AND_NULL(batup_alt);
-
-	/* Batch download vars */
-	for(i=0;i<cfg.max_batdn && batdn_name!=NULL;i++)
-		FREE_AND_NULL(batdn_name[i]);
-
-	FREE_AND_NULL(batdn_name);
-	FREE_AND_NULL(batdn_dir);
-	FREE_AND_NULL(batdn_offset);
-	FREE_AND_NULL(batdn_size);
-	FREE_AND_NULL(batdn_cdt);
-	FREE_AND_NULL(batdn_alt);
-
 	listFree(&savedlines);
 	listFree(&smb_list);
 	listFree(&mouse_hotspots);
@@ -4238,12 +4161,10 @@ void sbbs_t::reset_logon_vars(void)
     autoterm=0;
 	cterm_version = 0;
     lbuflen=0;
-    altul=0;
     timeleft_warn=0;
 	keybufbot=keybuftop=0;
     logon_uls=logon_ulb=logon_dls=logon_dlb=0;
     logon_posts=logon_emails=logon_fbacks=0;
-    batdn_total=batup_total=0;
     usrgrps=usrlibs=0;
     curgrp=curlib=0;
 	for(i=0;i<cfg.total_libs;i++)
@@ -4357,10 +4278,9 @@ void sbbs_t::logoffstats()
 			stats.ttoday+=(uint32_t)(now-logontime)/60;
 			stats.ptoday+=logon_posts;
 		}
-		stats.uls+=logon_uls;
-		stats.ulb+=logon_ulb;
-		stats.dls+=logon_dls;
-		stats.dlb+=logon_dlb;
+		stats.uls+=(uint32_t)logon_uls;
+		stats.ulb+=(uint32_t)logon_ulb;
+		// logon_dls and logons_dlb are now handled in user_downloaded_file()
 		stats.etoday+=logon_emails;
 		stats.ftoday+=logon_fbacks;
 
@@ -4965,14 +4885,6 @@ void DLLCALL bbs_thread(void* arg)
 
 	status("Initializing");
 
-	/* Defeat the lameo hex0rs - the name and copyright must remain intact */
-	if(crc32(COPYRIGHT_NOTICE,0)!=COPYRIGHT_CRC
-		|| crc32(VERSION_NOTICE,10)!=SYNCHRONET_CRC) {
-		lprintf(LOG_CRIT,"!CORRUPTED LIBRARY FILE");
-		cleanup(1);
-		return;
-	}
-
 	memset(text, 0, sizeof(text));
     memset(&scfg, 0, sizeof(scfg));
 
diff --git a/src/sbbs3/makeuser.vcxproj b/src/sbbs3/makeuser.vcxproj
index 8588ea262e1b9ea1d0cff24bc6720b536642f2ac..551490ad332bc8dc764cdd043e1a558f2a49d5e4 100644
--- a/src/sbbs3/makeuser.vcxproj
+++ b/src/sbbs3/makeuser.vcxproj
@@ -94,6 +94,7 @@
       </DataExecutionPrevention>
       <TargetMachine>MachineX86</TargetMachine>
       <ImageHasSafeExceptionHandlers>false</ImageHasSafeExceptionHandlers>
+      <AdditionalDependencies>netapi32.lib;wsock32.lib;%(AdditionalDependencies)</AdditionalDependencies>
     </Link>
     <Bscmake>
       <SuppressStartupBanner>true</SuppressStartupBanner>
@@ -137,6 +138,7 @@
       </DataExecutionPrevention>
       <TargetMachine>MachineX86</TargetMachine>
       <ImageHasSafeExceptionHandlers>false</ImageHasSafeExceptionHandlers>
+      <AdditionalDependencies>netapi32.lib;wsock32.lib;%(AdditionalDependencies)</AdditionalDependencies>
     </Link>
     <Bscmake>
       <SuppressStartupBanner>true</SuppressStartupBanner>
diff --git a/src/sbbs3/msg_id.c b/src/sbbs3/msg_id.c
index cfde9524672d0bb773c08062bb02b84db23c189a..471f9fd9928c2263b52de2bbad6b60a424a6982e 100644
--- a/src/sbbs3/msg_id.c
+++ b/src/sbbs3/msg_id.c
@@ -21,7 +21,8 @@
 
 #include "msg_id.h"
 #include "smblib.h"
-#include "ver.h"
+#include "git_branch.h"
+#include "git_hash.h"
 
 static ulong msg_number(smbmsg_t* msg)
 {
@@ -244,7 +245,7 @@ char* DLLCALL msg_program_id(char* pid, size_t maxlen)
 	DESCRIBE_COMPILER(compiler);
 	snprintf(pid, maxlen, "%.10s %s%c-%s %s/%s %s %s"
 		,VERSION_NOTICE,VERSION,REVISION,PLATFORM_DESC
-		,git_branch, git_hash
+		,GIT_BRANCH, GIT_HASH
 		,__DATE__,compiler);
 	return pid;
 }
diff --git a/src/sbbs3/msgdate.c b/src/sbbs3/msgdate.c
index d2ce64393360465980e88f7a2fff8f1727bb3e45..a09e71d9f89ce23fc330f507a012a1486458fed3 100644
--- a/src/sbbs3/msgdate.c
+++ b/src/sbbs3/msgdate.c
@@ -161,11 +161,3 @@ when_t DLLCALL rfc822date(char* date)
 
 	return(when);
 }
-
-BOOL DLLCALL newmsgs(smb_t* smb, time_t t)
-{
-	char index_fname[MAX_PATH + 1];
-
-	SAFEPRINTF(index_fname, "%s.sid", smb->file);
-	return fdate(index_fname) >= t;
-}
diff --git a/src/sbbs3/msgdate.h b/src/sbbs3/msgdate.h
index 5df0564ff41b09bfa76eb1d8edaa95ad906ec973..70d2ff6b49217859b0fe0ba0d2479fd7854de783 100644
--- a/src/sbbs3/msgdate.h
+++ b/src/sbbs3/msgdate.h
@@ -33,9 +33,8 @@ extern "C" {
 
 DLLEXPORT when_t	rfc822date(char* p);
 DLLEXPORT char *	msgdate(when_t when, char* buf);
-DLLEXPORT BOOL		newmsgs(smb_t*, time_t);
 
 #ifdef __cplusplus
 }
 #endif
-#endif /* Don't add anything after this line */
\ No newline at end of file
+#endif /* Don't add anything after this line */
diff --git a/src/sbbs3/netmail.cpp b/src/sbbs3/netmail.cpp
index 248dbc0378ef2b126b0092e9485faac1f9db3fc3..733465d5d14c285aec2281daf307f2d73a0eabed 100644
--- a/src/sbbs3/netmail.cpp
+++ b/src/sbbs3/netmail.cpp
@@ -367,7 +367,8 @@ void sbbs_t::qwktonetmail(FILE *rep, char *block, char *into, uchar fromhub)
 	int 	i,fido,inet=0,qnet=0;
 	uint16_t net;
 	uint16_t xlat;
-	long	l,offset,length,m,n;
+	long	l,length,m,n;
+	off_t offset;
 	faddr_t fidoaddr;
     fmsghdr_t hdr;
 	smbmsg_t msg;
@@ -709,7 +710,7 @@ void sbbs_t::qwktonetmail(FILE *rep, char *block, char *into, uchar fromhub)
 			smb_fputc(ch,smb.sdt_fp);
 		smb_fflush(smb.sdt_fp);
 
-		msg.hdr.offset=offset;
+		msg.hdr.offset=(uint32_t)offset;
 
 		smb_dfield(&msg,TEXT_BODY,length);
 
@@ -1099,7 +1100,7 @@ bool sbbs_t::inetmail(const char *into, const char *subj, long mode, smb_t* resm
 		errormsg(WHERE,ERR_OPEN,msgpath,O_RDONLY|O_BINARY);
 		return(false); 
 	}
-	off_t length = filelength(file);
+	long length = (long)filelength(file);
 	if(length < 1) {
 		strListFree(&rcpt_list);
 		fclose(instream);
@@ -1263,7 +1264,8 @@ bool sbbs_t::qnetmail(const char *into, const char *subj, long mode, smb_t* resm
 	const char*	charset=NULL;
 	ushort	xlat=XLAT_NONE,net=NET_QWK,touser;
 	int 	i,j,x,file;
-	ulong	length,offset;
+	ulong	length;
+	off_t offset;
 	FILE	*instream;
 	smbmsg_t msg;
 
@@ -1384,7 +1386,7 @@ bool sbbs_t::qnetmail(const char *into, const char *subj, long mode, smb_t* resm
 	}
 
 	setvbuf(instream,NULL,_IOFBF,2*1024);
-	fseek(smb.sdt_fp,offset,SEEK_SET);
+	fseeko(smb.sdt_fp,offset,SEEK_SET);
 	xlat=XLAT_NONE;
 	fwrite(&xlat,2,1,smb.sdt_fp);
 	x=SDT_BLOCK_LEN-2;				/* Don't read/write more than 255 */
@@ -1408,7 +1410,7 @@ bool sbbs_t::qnetmail(const char *into, const char *subj, long mode, smb_t* resm
 	msg.hdr.when_written.time=msg.hdr.when_imported.time=time32(NULL);
 	msg.hdr.when_written.zone=msg.hdr.when_imported.zone=sys_timezone(&cfg);
 
-	msg.hdr.offset=offset;
+	msg.hdr.offset=(uint32_t)offset;
 
 	net=NET_QWK;
 	smb_hfield_str(&msg,RECIPIENT,to);
diff --git a/src/sbbs3/node.c b/src/sbbs3/node.c
index e23fd52e8c13a750236970bb0e4c673d54510ca2..5de4ef2cd1199c376eb3b0c84c3763bfc64c89c4 100644
--- a/src/sbbs3/node.c
+++ b/src/sbbs3/node.c
@@ -517,7 +517,7 @@ int main(int argc, char **argv)
 	sprintf(str,"%snode.exb",ctrl_dir);
 	nodeexb=sopen(str,O_RDWR|O_BINARY,SH_DENYNO);
 
-	sys_nodes=filelength(nodefile)/sizeof(node_t);
+	sys_nodes=(int)(filelength(nodefile)/sizeof(node_t));
 	if(!sys_nodes) {
 		printf("%s reflects 0 nodes!\n",str);
 		exit(1); }
diff --git a/src/sbbs3/nodedefs.h b/src/sbbs3/nodedefs.h
index 7e3a8292521070403b15586a6b9ec6d9f10e3322..31b7c97f21785e5111feab06f1e94c703e741559 100644
--- a/src/sbbs3/nodedefs.h
+++ b/src/sbbs3/nodedefs.h
@@ -108,13 +108,11 @@ enum node_action {                  /* Node Action */
 	,NODE_LAST_ACTION				/* Must be last */
     };
 
-#if defined(_WIN32) || defined(__BORLANDC__)	/* necessary for compatibility with SBBS v2 */
-	#pragma pack(push,1)
-#endif
+#pragma pack(push,1)
 
 #define SIZEOF_NODE_T 15			/* Must == sizeof(node_t) */
 
-typedef struct _PACK {					/* Node information kept in node.dab */
+typedef struct {					/* Node information kept in node.dab */
 	uchar		status,                 /* Current Status of Node (enum node_status) */
 				errors,                 /* Number of Critical Errors */
 				action;                 /* Action User is doing on Node (enum node_action) */
@@ -130,8 +128,6 @@ typedef struct _PACK {					/* Node information kept in node.dab */
     uint32_t   extaux;					/* Extended aux dword for node */
 	} node_t;
 
-#if defined(_WIN32) || defined(__BORLANDC__)
 #pragma pack(pop)		/* original packing */
-#endif
 
 #endif /* Don't add anything after this line */
diff --git a/src/sbbs3/objects.mk b/src/sbbs3/objects.mk
index e88c6d743488d9e3a29ff2054eb75978f297d2d5..159992113169ec11b269218fe90224b7934fb168 100644
--- a/src/sbbs3/objects.mk
+++ b/src/sbbs3/objects.mk
@@ -41,6 +41,7 @@ OBJS	=	$(MTOBJODIR)$(DIRSEP)ansiterm$(OFILE) \
 			$(MTOBJODIR)$(DIRSEP)inkey$(OFILE)\
 			$(MTOBJODIR)$(DIRSEP)ident$(OFILE)\
 			$(MTOBJODIR)$(DIRSEP)jsdebug$(OFILE)\
+			$(MTOBJODIR)$(DIRSEP)js_archive$(OFILE)\
 			$(MTOBJODIR)$(DIRSEP)js_bbs$(OFILE)\
 			$(MTOBJODIR)$(DIRSEP)js_client$(OFILE)\
 			$(MTOBJODIR)$(DIRSEP)js_com$(OFILE)\
@@ -54,6 +55,7 @@ OBJS	=	$(MTOBJODIR)$(DIRSEP)ansiterm$(OFILE) \
 			$(MTOBJODIR)$(DIRSEP)js_internal$(OFILE)\
 			$(MTOBJODIR)$(DIRSEP)js_msg_area$(OFILE)\
 			$(MTOBJODIR)$(DIRSEP)js_msgbase$(OFILE)\
+			$(MTOBJODIR)$(DIRSEP)js_filebase$(OFILE)\
 			$(MTOBJODIR)$(DIRSEP)js_queue$(OFILE)\
 			$(MTOBJODIR)$(DIRSEP)js_request$(OFILE)\
 			$(MTOBJODIR)$(DIRSEP)js_rtpool$(OFILE)\
@@ -95,7 +97,6 @@ OBJS	=	$(MTOBJODIR)$(DIRSEP)ansiterm$(OFILE) \
 			$(MTOBJODIR)$(DIRSEP)scfglib2$(OFILE)\
 			$(MTOBJODIR)$(DIRSEP)scfgsave$(OFILE)\
 			$(MTOBJODIR)$(DIRSEP)sockopts$(OFILE)\
-			$(MTOBJODIR)$(DIRSEP)sortdir$(OFILE)\
 			$(MTOBJODIR)$(DIRSEP)str$(OFILE)\
 			$(MTOBJODIR)$(DIRSEP)str_util$(OFILE)\
 			$(MTOBJODIR)$(DIRSEP)telgate$(OFILE)\
@@ -174,7 +175,6 @@ SMBUTIL_OBJS = \
 
 SBBSECHO_OBJS = \
 			$(OBJODIR)$(DIRSEP)sbbsecho$(OFILE) \
-			$(OBJODIR)$(DIRSEP)ver$(OFILE) \
 			$(OBJODIR)$(DIRSEP)ars$(OFILE) \
 			$(OBJODIR)$(DIRSEP)date_str$(OFILE) \
 			$(OBJODIR)$(DIRSEP)load_cfg$(OFILE) \
@@ -184,6 +184,7 @@ SBBSECHO_OBJS = \
 			$(OBJODIR)$(DIRSEP)nopen$(OFILE) \
 			$(OBJODIR)$(DIRSEP)str_util$(OFILE) \
 			$(OBJODIR)$(DIRSEP)dat_rec$(OFILE) \
+			$(OBJODIR)$(DIRSEP)filedat$(OFILE) \
 			$(OBJODIR)$(DIRSEP)userdat$(OFILE) \
 			$(OBJODIR)$(DIRSEP)rechocfg$(OFILE) \
 			$(OBJODIR)$(DIRSEP)msg_id$(OFILE) \
@@ -222,7 +223,8 @@ FILELIST_OBJS = \
 			$(OBJODIR)$(DIRSEP)nopen$(OFILE) \
 			$(OBJODIR)$(DIRSEP)str_util$(OFILE) \
 			$(OBJODIR)$(DIRSEP)dat_rec$(OFILE) \
-			$(OBJODIR)$(DIRSEP)filedat$(OFILE)
+			$(OBJODIR)$(DIRSEP)filedat$(OFILE) \
+			$(OBJODIR)$(DIRSEP)userdat$(OFILE)
 
 MAKEUSER_OBJS = \
 			$(OBJODIR)$(DIRSEP)makeuser$(OFILE) \
@@ -248,6 +250,7 @@ JSDOOR_OBJS = \
 			$(MTOBJODIR)$(DIRSEP)dat_rec$(OFILE) \
 			$(MTOBJODIR)$(DIRSEP)jsdoor$(OFILE) \
 			$(MTOBJODIR)$(DIRSEP)jsdebug$(OFILE) \
+			$(MTOBJODIR)$(DIRSEP)js_archive$(OFILE) \
 			$(MTOBJODIR)$(DIRSEP)js_uifc$(OFILE) \
 			$(MTOBJODIR)$(DIRSEP)js_conio$(OFILE) \
 			$(MTOBJODIR)$(DIRSEP)js_request$(OFILE) \
@@ -317,6 +320,7 @@ DELFILES_OBJS = \
 			$(OBJODIR)$(DIRSEP)ars$(OFILE) \
 			$(OBJODIR)$(DIRSEP)nopen$(OFILE) \
 			$(OBJODIR)$(DIRSEP)filedat$(OFILE) \
+			$(OBJODIR)$(DIRSEP)userdat$(OFILE) \
 			$(OBJODIR)$(DIRSEP)dat_rec$(OFILE)
 
 DUPEFIND_OBJS = \
@@ -353,3 +357,14 @@ PKTDUMP_OBJS =		$(OBJODIR)$(DIRSEP)pktdump$(OFILE)
 
 FMSGDUMP_OBJS = 	$(OBJODIR)$(DIRSEP)fmsgdump$(OFILE)
 
+UPGRADE_TO_V319_OBJS =	$(OBJODIR)$(DIRSEP)upgrade_to_v319$(OFILE) \
+			$(OBJODIR)$(DIRSEP)filedat$(OFILE) \
+			$(OBJODIR)$(DIRSEP)userdat$(OFILE) \
+			$(OBJODIR)$(DIRSEP)dat_rec$(OFILE) \
+			$(OBJODIR)$(DIRSEP)load_cfg$(OFILE) \
+			$(OBJODIR)$(DIRSEP)scfglib1$(OFILE) \
+			$(OBJODIR)$(DIRSEP)scfglib2$(OFILE) \
+			$(OBJODIR)$(DIRSEP)str_util$(OFILE) \
+			$(OBJODIR)$(DIRSEP)ars$(OFILE) \
+			$(OBJODIR)$(DIRSEP)nopen$(OFILE)
+
diff --git a/src/sbbs3/pack_qwk.cpp b/src/sbbs3/pack_qwk.cpp
index 6c3371a1c1e08516cbe082bbbc68b01747698a1e..80975d81880ed552b8fc382a0ef2e78ae3bb7a5e 100644
--- a/src/sbbs3/pack_qwk.cpp
+++ b/src/sbbs3/pack_qwk.cpp
@@ -21,6 +21,7 @@
 
 #include "sbbs.h"
 #include "qwk.h"
+#include "filedat.h"
 
 /****************************************************************************/
 /* Creates QWK packet, returning 1 if successful, 0 if not. 				*/
@@ -28,7 +29,9 @@
 bool sbbs_t::pack_qwk(char *packet, ulong *msgcnt, bool prepack)
 {
 	char	str[MAX_PATH+1],ch;
-	char 	tmp[MAX_PATH+1],tmp2[MAX_PATH+1];
+	char 	tmp[MAX_PATH+1];
+	char	path[MAX_PATH+1];
+	char	error[256];
 	char*	fname;
 	int 	mode;
 	uint	i,j,k,conf;
@@ -36,8 +39,7 @@ bool sbbs_t::pack_qwk(char *packet, ulong *msgcnt, bool prepack)
 	uint32_t posts;
 	uint32_t mailmsgs=0;
 	uint32_t u;
-	ulong	totalcdt,totaltime
-			,files,submsgs,msgs,netfiles=0,preqwk=0;
+	ulong	files,submsgs,msgs,netfiles=0,preqwk=0;
 	uint32_t	lastmsg;
 	ulong	subs_scanned=0;
 	float	f;	/* Sparky is responsible */
@@ -67,17 +69,31 @@ bool sbbs_t::pack_qwk(char *packet, ulong *msgcnt, bool prepack)
 	delfiles(cfg.temp_dir,ALLFILES);
 	SAFEPRINTF2(str,"%sfile/%04u.qwk",cfg.data_dir,useron.number);
 	if(fexistcase(str)) {
-		for(k=0;k<cfg.total_fextrs;k++)
-			if(!stricmp(cfg.fextr[k]->ext,useron.tmpext)
-				&& chk_ar(cfg.fextr[k]->ar,&useron,&client))
-				break;
-		if(k>=cfg.total_fextrs)
-			k=0;
-		p=cmdstr(cfg.fextr[k]->cmd,str,ALLFILES,NULL);
-		if((i=external(p,ex))==0)
-			preqwk=1; 
-		else 
-			errormsg(WHERE,ERR_EXEC,p,i);
+		long file_count = extract_files_from_archive(str
+			,/* outdir: */cfg.temp_dir
+			,/* allowed_filename_chars: */NULL /* any */
+			,/* with_path: */false
+			,/* max_files: */0 /* unlimited */
+			,/* file_list: */NULL /* all files */
+			,error, sizeof(error));
+		if(file_count > 0) {
+			lprintf(LOG_DEBUG, "libarchive extracted %lu files from %s", file_count, str);
+			preqwk = TRUE;
+		} else {
+			if(*error)
+				lprintf(LOG_NOTICE, "libarchive error (%s) extracting %s", error, str);
+			for(k=0;k<cfg.total_fextrs;k++)
+				if(!stricmp(cfg.fextr[k]->ext,useron.tmpext)
+					&& chk_ar(cfg.fextr[k]->ar,&useron,&client))
+					break;
+			if(k>=cfg.total_fextrs)
+				k=0;
+			p=cmdstr(cfg.fextr[k]->cmd,str,ALLFILES,NULL);
+			if((i=external(p,ex))==0)
+				preqwk=1; 
+			else 
+				errormsg(WHERE,ERR_EXEC,p,i);
+		}
 	}
 
 	if(useron.qwk&QWK_EXPCTLA)
@@ -616,11 +632,11 @@ bool sbbs_t::pack_qwk(char *packet, ulong *msgcnt, bool prepack)
 			SAFEPRINTF3(str,"%sqnet/%s.out/%s",cfg.data_dir,id,dirent->d_name);
 			if(isdir(str))
 				continue;
-			SAFEPRINTF2(tmp2,"%s%s",cfg.temp_dir,dirent->d_name);
+			SAFEPRINTF2(path,"%s%s",cfg.temp_dir,dirent->d_name);
 			lncntr=0;	/* Defeat pause */
 			lprintf(LOG_INFO,"Including %s in packet",str);
 			bprintf(text[RetrievingFile],str);
-			if(!mv(str,tmp2,/* copy: */TRUE))
+			if(!mv(str,path,/* copy: */TRUE))
 				netfiles++;
 		}
 		if(dir!=NULL)
@@ -628,47 +644,40 @@ bool sbbs_t::pack_qwk(char *packet, ulong *msgcnt, bool prepack)
 		if(netfiles)
 			CRLF; 
 	}
-
-	if(batdn_total) {
-		for(i=0,totalcdt=0;i<batdn_total;i++)
-			if(!is_download_free(&cfg,batdn_dir[i],&useron,&client))
-				totalcdt+=batdn_cdt[i];
-		if(totalcdt>useron.cdt+useron.freecdt) {
-			bprintf(text[YouOnlyHaveNCredits]
-				,ultoac(useron.cdt+useron.freecdt,tmp)); 
-		}
-		else {
-			for(i=0,totaltime=0;i<batdn_total;i++) {
-				if(!(cfg.dir[batdn_dir[i]]->misc&DIR_TFREE) && cur_cps)
-					totaltime+=batdn_size[i]/(ulong)cur_cps; 
+	{
+		int64_t totalcdt = 0;
+		str_list_t ini = batch_list_read(&cfg, useron.number, XFER_BATCH_DOWNLOAD);
+		str_list_t filenames = iniGetSectionList(ini, NULL);
+		for(size_t i = 0; filenames[i] != NULL; i++) {
+			const char* filename = filenames[i];
+			file_t f = {{}};
+			if(!batch_file_load(&cfg, ini, filename, &f))
+				continue;
+			if(!is_download_free(&cfg, f.dir, &useron, &client)) {
+				if(totalcdt + f.cost > (int64_t)(useron.cdt+useron.freecdt)) {
+					bprintf(text[YouOnlyHaveNCredits]
+						,ultoac(useron.cdt+useron.freecdt,tmp));
+					batch_file_remove(&cfg, useron.number, XFER_BATCH_DOWNLOAD, filename);
+					continue;
+				}
+				totalcdt += f.cost;
 			}
-			if(!(useron.exempt&FLAG('T')) && !SYSOP && totaltime>timeleft)
-				bputs(text[NotEnoughTimeToDl]);
-			else {
-				for(i=0;i<batdn_total;i++) {
-					lncntr=0;
-					unpadfname(batdn_name[i],tmp);
-					SAFEPRINTF2(tmp2,"%s%s",cfg.temp_dir,tmp);
-					if(!fexistcase(tmp2)) {
-						seqwait(cfg.dir[batdn_dir[i]]->seqdev);
-						bprintf(text[RetrievingFile],tmp);
-						SAFEPRINTF2(str,"%s%s"
-							,batdn_alt[i]>0 && batdn_alt[i]<=cfg.altpaths
-							? cfg.altpath[batdn_alt[i]-1]
-							: cfg.dir[batdn_dir[i]]->path
-							,tmp);
-						mv(str,tmp2,/* copy: */TRUE); /* copy the file to temp dir */
-						getnodedat(cfg.node_num,&thisnode,/* copy: */TRUE);
-						thisnode.aux=0xfe;
-						putnodedat(cfg.node_num,&thisnode);
-						CRLF; 
-					} 
-				} 
-			} 
-		} 
+			lncntr=0;
+			SAFEPRINTF2(tmp, "%s%s", cfg.temp_dir, filename);
+			if(!fexistcase(tmp)) {
+				seqwait(cfg.dir[f.dir]->seqdev);
+				getfilepath(&cfg, &f, path);
+				bprintf(text[RetrievingFile], path);
+				if(mv(path, tmp,/* copy: */TRUE) != 0) /* copy the file to temp dir */
+					batch_file_remove(&cfg, useron.number, XFER_BATCH_DOWNLOAD, filename);
+				CRLF;
+			}
+		}
+		iniFreeStringList(ini);
+		iniFreeStringList(filenames);
 	}
 
-	if(!(*msgcnt) && !mailmsgs && !files && !netfiles && !batdn_total && !voting_data
+	if(!(*msgcnt) && !mailmsgs && !files && !netfiles && !batdn_total() && !voting_data
 		&& (prepack || !preqwk)) {
 		if(online == ON_REMOTE)
 			bputs(text[QWKNoNewMessages]);
@@ -681,28 +690,28 @@ bool sbbs_t::pack_qwk(char *packet, ulong *msgcnt, bool prepack)
 		/***********************/
 		SAFEPRINTF(str,"%sQWK/HELLO",cfg.text_dir);
 		if(fexistcase(str)) {
-			SAFEPRINTF(tmp2,"%sHELLO",cfg.temp_dir);
-			mv(str,tmp2,/* copy: */TRUE); 
+			SAFEPRINTF(path,"%sHELLO",cfg.temp_dir);
+			mv(str,path,/* copy: */TRUE); 
 		}
 		SAFEPRINTF(str,"%sQWK/BBSNEWS",cfg.text_dir);
 		if(fexistcase(str)) {
-			SAFEPRINTF(tmp2,"%sBBSNEWS",cfg.temp_dir);
-			mv(str,tmp2,/* copy: */TRUE); 
+			SAFEPRINTF(path,"%sBBSNEWS",cfg.temp_dir);
+			mv(str,path,/* copy: */TRUE); 
 		}
 		SAFEPRINTF(str,"%sQWK/GOODBYE",cfg.text_dir);
 		if(fexistcase(str)) {
-			SAFEPRINTF(tmp2,"%sGOODBYE",cfg.temp_dir);
-			mv(str,tmp2,/* copy: */TRUE); 
+			SAFEPRINTF(path,"%sGOODBYE",cfg.temp_dir);
+			mv(str,path,/* copy: */TRUE); 
 		}
 		SAFEPRINTF(str,"%sQWK/BLT-*",cfg.text_dir);
 		glob(str,0,NULL,&g);
 		for(i=0;i<(uint)g.gl_pathc;i++) { 			/* Copy BLT-*.* files */
 			fname=getfname(g.gl_pathv[i]);
-			padfname(fname,str);
-			if(IS_DIGIT(str[4]) && IS_DIGIT(str[9])) {
+			char* fext = getfext(fname);
+			if(IS_DIGIT(str[4]) && fext != NULL && IS_DIGIT(*(fext + 1))) {
 				SAFEPRINTF2(str,"%sQWK/%s",cfg.text_dir,fname);
-				SAFEPRINTF2(tmp2,"%s%s",cfg.temp_dir,fname);
-				mv(str,tmp2,/* copy: */TRUE); 
+				SAFEPRINTF2(path,"%s%s",cfg.temp_dir,fname);
+				mv(str,path,/* copy: */TRUE); 
 			}
 		}
 		globfree(&g);
@@ -718,15 +727,21 @@ bool sbbs_t::pack_qwk(char *packet, ulong *msgcnt, bool prepack)
 	/*******************/
 	/* Compress Packet */
 	/*******************/
-	SAFEPRINTF2(tmp2,"%s%s",cfg.temp_dir,ALLFILES);
-	i=external(cmdstr(temp_cmd(),packet,tmp2,NULL)
-		,ex|EX_WILDCARD);
+	SAFEPRINTF2(path,"%s%s",cfg.temp_dir,ALLFILES);
+	if(strListFind((str_list_t)supported_archive_formats, useron.tmpext, /* case_sensitive */FALSE) >= 0) {
+		str_list_t file_list = directory(path);
+		long file_count = create_archive(packet, useron.tmpext, /* with_path: */false, file_list, error, sizeof(error));
+		strListFree(&file_list);
+		if(file_count < 0)
+			lprintf(LOG_ERR, "libarchive error (%s) creating %s", error, packet);
+		else
+			lprintf(LOG_INFO, "libarchive created %s from %ld files", packet, file_count);
+	} else {
+		if((i = external(cmdstr(temp_cmd(),packet,path,NULL), ex|EX_WILDCARD)) != 0)
+			errormsg(WHERE,ERR_EXEC,cmdstr(temp_cmd(),packet,path,NULL),i);
+	}
 	if(!fexist(packet)) {
 		bputs(text[QWKCompressionFailed]);
-		if(i)
-			errormsg(WHERE,ERR_EXEC,cmdstr(temp_cmd(),packet,tmp2,NULL),i);
-		else
-			lprintf(LOG_ERR, "Couldn't compress QWK packet");
 		return(false); 
 	}
 
diff --git a/src/sbbs3/pack_rep.cpp b/src/sbbs3/pack_rep.cpp
index 86b3e5297d34aa2c686fc9205343795f44887b3d..190bba9ebbbc6ec16c001f0a2b3502386651d52c 100644
--- a/src/sbbs3/pack_rep.cpp
+++ b/src/sbbs3/pack_rep.cpp
@@ -21,6 +21,7 @@
 
 #include "sbbs.h"
 #include "qwk.h"
+#include "filedat.h"
 
 /****************************************************************************/
 /* Creates an REP packet for upload to QWK hub 'hubnum'.                    */
@@ -32,6 +33,7 @@ bool sbbs_t::pack_rep(uint hubnum)
 	char 		tmp[MAX_PATH+1],tmp2[MAX_PATH+1];
 	char		hubid_upper[LEN_QWKID+1];
 	char		hubid_lower[LEN_QWKID+1];
+	char		error[256];
 	int 		mode;
 	const char* fmode;
 	uint		i,j,k;
@@ -61,7 +63,21 @@ bool sbbs_t::pack_rep(uint hubnum)
 	SAFEPRINTF2(str,"%s%s.REP",cfg.data_dir,hubid_upper);
 	if(fexistcase(str)) {
 		lprintf(LOG_INFO,"Updating %s", str);
-		external(cmdstr(cfg.qhub[hubnum]->unpack,str,ALLFILES,NULL),EX_OFFLINE);
+		long file_count = extract_files_from_archive(str
+			,/* outdir: */cfg.temp_dir
+			,/* allowed_filename_chars: */NULL /* any */
+			,/* with_path: */false
+			,/* max_files: */0 /* unlimited */
+			,/* file_list: */NULL /* all files */
+			,error, sizeof(error));
+		if(file_count > 0) {
+			lprintf(LOG_DEBUG, "libarchive extracted %lu files from %s", file_count, str);
+		} else {
+			if(*error)
+				lprintf(LOG_NOTICE, "libarchive error (%s) extracting %s", error, str);
+			if(*cfg.qhub[hubnum]->unpack)
+				external(cmdstr(cfg.qhub[hubnum]->unpack,str,ALLFILES,NULL),EX_OFFLINE);
+		}
 	} else
 		lprintf(LOG_INFO,"Creating %s", str);
 	/*************************************************/
@@ -268,14 +284,23 @@ bool sbbs_t::pack_rep(uint hubnum)
 	/*******************/
 	SAFEPRINTF2(str,"%s%s.REP",cfg.data_dir,hubid_upper);
 	SAFEPRINTF2(tmp2,"%s%s",cfg.temp_dir,ALLFILES);
-	i=external(cmdstr(cfg.qhub[hubnum]->pack,str,tmp2,NULL)
-		,EX_OFFLINE|EX_WILDCARD);
-	if(!fexistcase(str)) {
-		lprintf(LOG_WARNING,"%s",remove_ctrl_a(text[QWKCompressionFailed],tmp));
+	if(strListFind((str_list_t)supported_archive_formats, cfg.qhub[hubnum]->fmt, /* case_sensitive */FALSE) >= 0) {
+		str_list_t file_list = directory(tmp2);
+		long file_count = create_archive(str, cfg.qhub[hubnum]->fmt, /* with_path: */false, file_list, error, sizeof(error));
+		strListFree(&file_list);
+		if(file_count < 0)
+			lprintf(LOG_ERR, "libarchive error %ld (%s) creating %s", file_count, error, str);
+		else
+			lprintf(LOG_INFO, "libarchive created %s from %ld files", str, file_count);
+	} else {
+		i=external(cmdstr(cfg.qhub[hubnum]->pack,str,tmp2,NULL)
+			,EX_OFFLINE|EX_WILDCARD);
 		if(i)
 			errormsg(WHERE,ERR_EXEC,cmdstr(cfg.qhub[hubnum]->pack,str,tmp2,NULL),i);
-		else
-			lprintf(LOG_ERR, "Couldn't compress REP packet");
+	}
+	if(!fexistcase(str)) {
+		lprintf(LOG_WARNING,"%s",remove_ctrl_a(text[QWKCompressionFailed],tmp));
+		lprintf(LOG_ERR, "Couldn't compress REP packet");
 		return(false); 
 	}
 	SAFEPRINTF2(str,"%sqnet/%s.out/",cfg.data_dir,hubid_lower);
diff --git a/src/sbbs3/postmsg.cpp b/src/sbbs3/postmsg.cpp
index a370c45c492c8af06088f72915ce05146518e0b8..05a34a4f798f23fefb0b6d741d1d7ba6cf650e8b 100644
--- a/src/sbbs3/postmsg.cpp
+++ b/src/sbbs3/postmsg.cpp
@@ -1,7 +1,4 @@
 /* Synchronet user create/post public message routine */
-// vi: tabstop=4
-
-/* $Id: postmsg.cpp,v 1.135 2020/08/15 21:58:14 rswindell Exp $ */
 
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
@@ -16,26 +13,15 @@
  * See the GNU General Public License for more details: gpl.txt or			*
  * http://www.fsf.org/copyleft/gpl.html										*
  *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
- *																			*
  * For Synchronet coding style and modification guidelines, see				*
  * http://www.synchro.net/source.html										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
 #include "sbbs.h"
 #include "utf8.h"
+#include "filedat.h"
 
 int msgbase_open(scfg_t* cfg, smb_t* smb, unsigned int subnum, int* storage, long* dupechk_hashes, uint16_t* xlat)
 {
@@ -378,9 +364,9 @@ extern "C" void DLLCALL signal_sub_sem(scfg_t* cfg, uint subnum)
 
 	/* signal semaphore files */
 	if(cfg->sub[subnum]->misc&SUB_FIDO && cfg->echomail_sem[0])		
-		ftouch(cmdstr(cfg,NULL,cfg->echomail_sem,nulstr,nulstr,str));
+		ftouch(cmdstr(cfg,NULL,cfg->echomail_sem,nulstr,nulstr,str,sizeof(str)));
 	if(cfg->sub[subnum]->post_sem[0]) 
-		ftouch(cmdstr(cfg,NULL,cfg->sub[subnum]->post_sem,nulstr,nulstr,str));
+		ftouch(cmdstr(cfg,NULL,cfg->sub[subnum]->post_sem,nulstr,nulstr,str,sizeof(str)));
 }
 
 extern "C" int DLLCALL msg_client_hfields(smbmsg_t* msg, client_t* client)
@@ -507,8 +493,10 @@ extern "C" int DLLCALL savemsg(scfg_t* cfg, smb_t* smb, smbmsg_t* msg, client_t*
 	if((i=smb_addmsg(smb,msg,smb_storage_mode(cfg, smb),dupechk_hashes,xlat,(uchar*)msgbuf, findsig(msgbuf)))==SMB_SUCCESS
 		&& msg->to!=NULL	/* no recipient means no header created at this stage */) {
 		if(smb->subnum == INVALID_SUB) {
-			if(msg->to_net.type == NET_FIDO && cfg->netmail_sem[0])
-				ftouch(cmdstr(cfg,NULL,cfg->netmail_sem,nulstr,nulstr,NULL));
+			if(msg->to_net.type == NET_FIDO && cfg->netmail_sem[0]) {
+				char tmp[MAX_PATH + 1];
+				ftouch(cmdstr(cfg,NULL,cfg->netmail_sem,nulstr,nulstr,tmp, sizeof(tmp)));
+			}
 		} else
 			signal_sub_sem(cfg,smb->subnum);
 
diff --git a/src/sbbs3/qwk.cpp b/src/sbbs3/qwk.cpp
index 32a6013e97dfcdd160238267f2b97f1bfd92c646..ff39c49ab869725faa784dc6d453846aa4776258 100644
--- a/src/sbbs3/qwk.cpp
+++ b/src/sbbs3/qwk.cpp
@@ -21,6 +21,7 @@
 
 #include "sbbs.h"
 #include "qwk.h"
+#include "filedat.h"
 
 /****************************************************************************/
 /* Converts a long to an msbin real number. required for QWK NDX file		*/
@@ -374,28 +375,21 @@ void sbbs_t::qwk_success(ulong msgcnt, char bi, char prepack)
 /****************************************************************************/
 void sbbs_t::qwk_sec()
 {
-	char	str[256],tmp2[256],ch,bi=0;
+	char	str[256],tmp2[256],ch;
 	char 	tmp[512];
 	int		error;
 	int 	s;
-	uint	i,k;
-	ulong	l;
+	uint	i;
 	ulong	msgcnt;
 	ulong	*sav_ptr;
-	file_t	fd;
 
-	memset(&fd,0,sizeof(fd));
 	getusrdirs();
-	fd.dir=cfg.total_dirs;
 	if((sav_ptr=(ulong *)malloc(sizeof(ulong)*cfg.total_subs))==NULL) {
 		errormsg(WHERE,ERR_ALLOC,nulstr,sizeof(ulong)*cfg.total_subs);
 		return;
 	}
 	for(i=0;i<cfg.total_subs;i++)
 		sav_ptr[i]=subscan[i].ptr;
-	for(i=0;i<cfg.total_prots;i++)
-		if(cfg.prot[i]->bicmd[0] && chk_ar(cfg.prot[i]->ar,&useron,&client))
-			bi++;				/* number of bidirectional protocols configured */
 	if(useron.rest&FLAG('Q'))
 		getusrsubs();
 	delfiles(cfg.temp_dir,ALLFILES);
@@ -407,8 +401,6 @@ void sbbs_t::qwk_sec()
 		ASYNC;
 		bputs(text[QWKPrompt]);
 		sprintf(str,"?UDCSP\r%c",text[YNQP][2]);
-		if(bi)
-			strcat(str,"B");
 		ch=(char)getkeys(str,0);
 		if(ch>' ')
 			logch(ch,0);
@@ -506,14 +498,23 @@ void sbbs_t::qwk_sec()
 							useron.qwk&=~(QWK_EXPCTLA|QWK_RETCTLA);
 						break;
 					case 'T':
-						for(i=0;i<cfg.total_fcomps;i++)
-							uselect(1,i,text[ArchiveTypeHeading],cfg.fcomp[i]->ext,cfg.fcomp[i]->ar);
+					{
+						str_list_t ext_list = strListDup((str_list_t)supported_archive_formats);
+						for(i=0; i < cfg.total_fcomps; i++) {
+							if(strListFind(ext_list, cfg.fcomp[i]->ext, /* case-sensitive */FALSE) < 0
+								&& chk_ar(cfg.fcomp[i]->ar, &useron, &client))
+								strListPush(&ext_list, cfg.fcomp[i]->ext);
+						}
+						for(i=0; ext_list[i] != NULL; i++)
+							uselect(1, i, text[ArchiveTypeHeading], ext_list[i], NULL);
 						s=uselect(0,0,0,0,0);
 						if(s>=0) {
-							strcpy(useron.tmpext,cfg.fcomp[s]->ext);
+							SAFECOPY(useron.tmpext, ext_list[s]);
 							putuserrec(&cfg,useron.number,U_TMPEXT,3,useron.tmpext);
 						}
+						strListFree(&ext_list);
 						break;
+					}
 					case 'E':
 						if(!(useron.qwk&(QWK_EMAIL|QWK_ALLMAIL)))
 							useron.qwk|=QWK_EMAIL;
@@ -571,82 +572,7 @@ void sbbs_t::qwk_sec()
 			continue;
 		}
 
-
-		if(ch=='B') {   /* Bidirectional QWK and REP packet transfer */
-			sprintf(str,"%s%s.qwk",cfg.temp_dir,cfg.sys_id);
-			if(!fexistcase(str) && !pack_qwk(str,&msgcnt,0)) {
-				for(i=0;i<cfg.total_subs;i++)
-					subscan[i].ptr=sav_ptr[i];
-				remove(str);
-				last_ns_time=ns_time;
-				continue;
-			}
-			bprintf(text[UploadingREP],cfg.sys_id);
-			xfer_prot_menu(XFER_BIDIR);
-			mnemonics(text[ProtocolOrQuit]);
-			sprintf(tmp2,"%c",text[YNQP][2]);
-			for(i=0;i<cfg.total_prots;i++)
-				if(cfg.prot[i]->bicmd[0] && chk_ar(cfg.prot[i]->ar,&useron,&client)) {
-					sprintf(tmp,"%c",cfg.prot[i]->mnemonic);
-					strcat(tmp2,tmp);
-				}
-			ch=(char)getkeys(tmp2,0);
-			if(ch==text[YNQP][2] || sys_status&SS_ABORT || !online) {
-				for(i=0;i<cfg.total_subs;i++)
-					subscan[i].ptr=sav_ptr[i];	/* re-load saved pointers */
-				last_ns_time=ns_time;
-				continue;
-			}
-			for(i=0;i<cfg.total_prots;i++)
-				if(cfg.prot[i]->bicmd[0] && cfg.prot[i]->mnemonic==ch
-					&& chk_ar(cfg.prot[i]->ar,&useron,&client))
-					break;
-			if(i<cfg.total_prots) {
-				batup_total=1;
-				batup_dir[0]=cfg.total_dirs;
-				sprintf(batup_name[0],"%s.rep",cfg.sys_id);
-				batdn_total=1;
-				batdn_dir[0]=cfg.total_dirs;
-				sprintf(batdn_name[0],"%s.qwk",cfg.sys_id);
-				if(!create_batchdn_lst((cfg.prot[i]->misc&PROT_NATIVE) ? true:false)
-					|| !create_batchup_lst()
-					|| !create_bimodem_pth()) {
-					batup_total=batdn_total=0;
-					continue;
-				}
-				sprintf(str,"%s%s.qwk",cfg.temp_dir,cfg.sys_id);
-				sprintf(tmp2,"%s.qwk",cfg.sys_id);
-				padfname(tmp2,fd.name);
-				sprintf(str,"%sBATCHDN.LST",cfg.node_dir);
-				sprintf(tmp2,"%sBATCHUP.LST",cfg.node_dir);
-				error=protocol(cfg.prot[i],XFER_BIDIR,str,tmp2,true);
-				batdn_total=batup_total=0;
-				if(!checkprotresult(cfg.prot[i],error,&fd)) {
-					last_ns_time=ns_time;
-					for(i=0;i<cfg.total_subs;i++)
-						subscan[i].ptr=sav_ptr[i]; /* re-load saved pointers */
-				}
-				else {
-					qwk_success(msgcnt,1,0);
-					for(i=0;i<cfg.total_subs;i++)
-						sav_ptr[i]=subscan[i].ptr;
-				}
-				sprintf(str,"%s%s.qwk",cfg.temp_dir,cfg.sys_id);
-				if(fexistcase(str))
-					remove(str);
-				unpack_rep();
-				delfiles(cfg.temp_dir,ALLFILES);
-				//autohangup();
-				}
-			else {
-				last_ns_time=ns_time;
-				for(i=0;i<cfg.total_subs;i++)
-					subscan[i].ptr=sav_ptr[i];
-			}
-
-		}
-
-		else if(ch=='D') {   /* Download QWK Packet of new messages */
+		if(ch=='D') {   /* Download QWK Packet of new messages */
 			sprintf(str,"%s%s.qwk",cfg.temp_dir,cfg.sys_id);
 			if(!fexistcase(str) && !pack_qwk(str,&msgcnt,0)) {
 				for(i=0;i<cfg.total_subs;i++)
@@ -656,12 +582,13 @@ void sbbs_t::qwk_sec()
 				continue;
 			}
 
-			l=(long)flength(str);
-			bprintf(text[FiFilename],getfname(str));
-			bprintf(text[FiFileSize],ultoac(l,tmp)
+			off_t l=flength(str);
+			bprintf(text[FiFilename], getfname(str));
+			bprintf(text[FiFileSize], ultoac((ulong)l,tmp)
 				, byte_estimate_to_str(l, tmp2, sizeof(tmp), /* units: */1024, /* precision: */1));
+
 			if(l>0L && cur_cps)
-				i=l/(ulong)cur_cps;
+				i=(uint)(l/(ulong)cur_cps);
 			else
 				i=0;
 			bprintf(text[FiTransferTime],sectostr(i,tmp));
@@ -696,9 +623,8 @@ void sbbs_t::qwk_sec()
 			if(i<cfg.total_prots) {
 				sprintf(str,"%s%s.qwk",cfg.temp_dir,cfg.sys_id);
 				sprintf(tmp2,"%s.qwk",cfg.sys_id);
-				padfname(tmp2,fd.name);
 				error=protocol(cfg.prot[i],XFER_DOWNLOAD,str,nulstr,false);
-				if(!checkprotresult(cfg.prot[i],error,&fd)) {
+				if(!checkprotresult(cfg.prot[i], error, tmp2)) {
 					last_ns_time=ns_time;
 					for(i=0;i<cfg.total_subs;i++)
 						subscan[i].ptr=sav_ptr[i]; /* re-load saved pointers */
@@ -726,15 +652,6 @@ void sbbs_t::qwk_sec()
 
 			delfiles(cfg.temp_dir,ALLFILES);
 			bprintf(text[UploadingREP],cfg.sys_id);
-			for(k=0;k<cfg.total_fextrs;k++)
-				if(!stricmp(cfg.fextr[k]->ext,useron.tmpext)
-					&& chk_ar(cfg.fextr[k]->ar,&useron,&client))
-					break;
-			if(k>=cfg.total_fextrs) {
-				bputs(text[QWKExtractionFailed]);
-				lprintf(LOG_ERR, "Couldn't extract REP packet - configuration error");
-				continue;
-			}
 
 			/******************/
 			/* Receive Packet */
@@ -802,7 +719,7 @@ void sbbs_t::qwkcfgline(char *buf,uint subnum)
 	uint 	x,y;
 	long	l;
 	ulong	qwk=useron.qwk;
-	file_t	f;
+	file_t f = {{}};
 
 	sprintf(str,"%-25.25s",buf);	/* Note: must be space-padded, left justified */
 	strupr(str);
@@ -968,29 +885,26 @@ void sbbs_t::qwkcfgline(char *buf,uint subnum)
 	}
 
 	else if(!strncmp(str,"FREQ ",5)) {                  /* file request */
-		padfname(str+5,f.name);
+		const char* fname = str + 5;
+		SKIP_WHITESPACE(fname);
 		for(x=y=0;x<usrlibs;x++) {
 			for(y=0;y<usrdirs[x];y++)
-				if(findfile(&cfg,usrdir[x][y],f.name))
+				if(loadfile(&cfg, usrdir[x][y], fname, &f, file_detail_normal))
 					break;
 			if(y<usrdirs[x])
 				break;
 		}
 		if(x>=usrlibs) {
-			bprintf("\r\n%s",f.name);
+			bprintf("\r\n%s",fname);
 			bputs(text[FileNotFound]);
 		}
 		else {
-			f.dir=usrdir[x][y];
-			getfileixb(&cfg,&f);
-			f.size=0;
-			getfiledat(&cfg,&f);
-			if(f.size==-1L)
-				bprintf(text[FileIsNotOnline],f.name);
+			if(getfilesize(&cfg, &f) < 0)
+				bprintf(text[FileIsNotOnline], f.name);
 			else
 				addtobatdl(&f);
 		}
-
+		smb_freefilemem(&f);
 	}
 
 	else {
diff --git a/src/sbbs3/qwknodes.c b/src/sbbs3/qwknodes.c
index ca13d0407f356974e05307e462a4c5ea29c55388..0133094c26d95aaf6a862fa8da0fad4e46de488b 100644
--- a/src/sbbs3/qwknodes.c
+++ b/src/sbbs3/qwknodes.c
@@ -275,18 +275,18 @@ int main(int argc, char **argv)
 			continue; 
 		}
 		smb_unlocksmbhdr(&smb);
-		msg.offset=smb.status.total_msgs;
-		if(!msg.offset) {
+		msg.idx_offset=smb.status.total_msgs;
+		if(!msg.idx_offset) {
 			smb_close(&smb);
 			printf("Empty.\n");
 			continue; 
 		}
-		while(!kbhit() && !ferror(smb.sid_fp) && msg.offset) {
-			msg.offset--;
-			fseek(smb.sid_fp,msg.offset*sizeof(idxrec_t),SEEK_SET);
+		while(!kbhit() && !ferror(smb.sid_fp) && msg.idx_offset) {
+			msg.idx_offset--;
+			fseek(smb.sid_fp,msg.idx_offset*sizeof(idxrec_t),SEEK_SET);
 			if(!fread(&msg.idx,1,sizeof(idxrec_t),smb.sid_fp))
 				break;
-			fprintf(stderr,"%-5u\r",msg.offset+1);
+			fprintf(stderr,"%-5u\r",msg.idx_offset+1);
 			if(msg.idx.to==smm || msg.idx.to==sbl)
 				continue;
 			if(max_age && now-msg.idx.time>((time_t)max_age*24UL*60UL*60UL))
diff --git a/src/sbbs3/readmsgs.cpp b/src/sbbs3/readmsgs.cpp
index 8f3572361ebcc11d3e8f912c226479a5154d770c..0e6793e45a1cad0bf6dce8d334ad94da4968fd08 100644
--- a/src/sbbs3/readmsgs.cpp
+++ b/src/sbbs3/readmsgs.cpp
@@ -474,6 +474,7 @@ int sbbs_t::scanposts(uint subnum, long mode, const char *find)
 		for(smb.curmsg=0;smb.curmsg<smb.msgs;smb.curmsg++)
 			if(subscan[subnum].ptr<post[smb.curmsg].idx.number)
 				break;
+		lncntr = 0;
 		bprintf(text[NScanStatusFmt]
 			,cfg.grp[cfg.sub[subnum]->grp]->sname,cfg.sub[subnum]->lname,smb.msgs-smb.curmsg,msgs);
 		if(!smb.msgs) {		  /* no messages at all */
@@ -494,6 +495,7 @@ int sbbs_t::scanposts(uint subnum, long mode, const char *find)
 	}
 	else {
 		cleartoeol();
+		lncntr = 0;
 		if(mode&SCAN_TOYOU)
 			bprintf(text[NScanStatusFmt]
 			   ,cfg.grp[cfg.sub[subnum]->grp]->sname,cfg.sub[subnum]->lname,smb.msgs,msgs);
diff --git a/src/sbbs3/release.bat b/src/sbbs3/release.bat
index 928d02cb898c6e0883ef9e15bc5e0b283ea832fa..e69ac8c0d56fe655b0b05b17690dbd1dd5b28421 100644
--- a/src/sbbs3/release.bat
+++ b/src/sbbs3/release.bat
@@ -1,2 +1,3 @@
 @echo off
+call gitinfo.bat
 call build.bat "/p:Configuration=Release" %*
\ No newline at end of file
diff --git a/src/sbbs3/sbbs.h b/src/sbbs3/sbbs.h
index baf519f70c037b5b4bbdf0edc2b5dd3cbcecc7a7..0fa8fb3938740cb4e54cd551db8777eeff18c63b 100644
--- a/src/sbbs3/sbbs.h
+++ b/src/sbbs3/sbbs.h
@@ -291,7 +291,6 @@ extern int	thread_suid_broken;			/* NPTL is no longer broken */
 #include "str_util.h"
 #include "date_str.h"
 #include "load_cfg.h"
-#include "filedat.h"
 #include "getstats.h"
 #include "msgdate.h"
 #include "getmail.h"
@@ -433,6 +432,7 @@ public:
 
     RingBuf	inbuf;
     RingBuf	outbuf;
+	bool	WaitForOutbufEmpty(int timeout) { return WaitForEvent(outbuf.empty_event, timeout) == WAIT_OBJECT_0; }
 	HANDLE	input_thread;
 	pthread_mutex_t	input_thread_mutex;
 	bool	input_thread_mutex_created;
@@ -515,23 +515,8 @@ public:
 	int 	nodefile;		/* File handle for node.dab */
 	pthread_mutex_t	nodefile_mutex;
 	int		node_ext;		/* File handle for node.exb */
-
-							/* Batch download queue */
-	char 	**batdn_name;	/* Filenames */
-	ushort	*batdn_alt; 	/* Alternate path */
-	uint 	*batdn_dir, 	/* Directory for each file */
-			 batdn_total;	/* Total files */
-	long 	*batdn_offset;	/* Offset for data */
-	ulong	*batdn_size;	/* Size of file in bytes */
-	ulong	*batdn_cdt; 	/* Credit value of file */
-
-							/* Batch upload queue */
-	char 	**batup_desc,	/* Description for each file */
-			**batup_name;	/* Filenames */
-	long	*batup_misc;	/* Miscellaneous bits */
-	ushort	*batup_alt; 	/* Alternate path */
-	uint 	*batup_dir, 	/* Directory for each file */
-			batup_total;	/* Total files */
+	size_t	batup_total();
+	size_t	batdn_total();
 
 	/*********************************/
 	/* Color Configuration Variables */
@@ -584,14 +569,14 @@ public:
 	long 	sys_status; 	/* System Status */
 	subscan_t	*subscan;	/* User sub configuration/scan info */
 
-	ulong	logon_ulb,		/* Upload Bytes This Call */
+	int64_t	logon_ulb,		/* Upload Bytes This Call */
 			logon_dlb,		/* Download Bytes This Call */
 			logon_uls,		/* Uploads This Call */
-			logon_dls,		/* Downloads This Call */
-			logon_posts,	/* Posts This Call */
+			logon_dls;		/* Downloads This Call */
+	ulong	logon_posts,	/* Posts This Call */
 			logon_emails,	/* Emails This Call */
 			logon_fbacks;	/* Feedbacks This Call */
-	uchar	logon_ml;		/* ML of the user upon logon */
+	uchar	logon_ml;		/* Security level of the user upon logon */
 
 	uint 	main_cmds;		/* Number of Main Commands this call */
 	uint 	xfer_cmds;		/* Number of Xfer Commands this call */
@@ -622,7 +607,6 @@ public:
 	ulong 	timeleft;		/* Number of seconds user has left online */
 
 	char 	*comspec;		/* Pointer to environment variable COMSPEC */
-	ushort	altul;			/* Upload to alternate path flag */
 	char 	cid[LEN_CID+1]; /* Caller ID (IP Address) of current caller */
 	char 	*noaccess_str;	/* Why access was denied via ARS */
 	long 	noaccess_val;	/* Value of parameter not met in ARS */
@@ -634,7 +618,7 @@ public:
 	const char*	current_msg_subj;
 	const char*	current_msg_from;
 	const char*	current_msg_to;
-	file_t*		current_file;
+	file_t*	current_file;	
 
 			/* Global command shell variables */
 	uint	global_str_vars;
@@ -668,7 +652,7 @@ public:
 			/* Command Shell Methods */
 	int		exec(csi_t *csi);
 	int		exec_function(csi_t *csi);
-	int		exec_misc(csi_t *csi, char *path);
+	int		exec_misc(csi_t *csi, const char *path);
 	int		exec_net(csi_t *csi);
 	int		exec_msg(csi_t *csi);
 	int		exec_file(csi_t *csi);
@@ -704,7 +688,6 @@ public:
 	int 	sub_op(uint subnum);
 
 	int		dir_op(uint dirnum);
-	int		getuserxfers(int fromuser, int destuser, char *fname);
 
 	void	getmsgptrs(void);
 	void	putmsgptrs(void);
@@ -808,7 +791,9 @@ public:
 	/* con_out.cpp */
 	size_t	bstrlen(const char *str, long mode = 0);
 	int		bputs(const char *str, long mode = 0);	/* BBS puts function */
+	int		bputs(long mode, const char* str) { return bputs(str, mode); }
 	int		rputs(const char *str, size_t len=0);	/* BBS raw puts function */
+	int		rputs(long mode, const char* str) { return rputs(str, mode); }
 	int		bprintf(const char *fmt, ...)			/* BBS printf function */
 #if defined(__GNUC__)   // Catch printf-format errors
     __attribute__ ((format (printf, 2, 3)));		// 1 is 'this'
@@ -851,6 +836,9 @@ public:
 	void	carriage_return(int count=1);
 	void	line_feed(int count=1);
 	void	newline(int count=1);
+	void	cond_newline() { if(column > 0) newline(); }
+	void	cond_blankline() { if(column > 0) newline(); if(lastlinelen) newline(); }
+	void	cond_contline() { if(column > 0 && cols < TERM_COLS_DEFAULT) bputs(text[LongLineContinuationPrefix]); }
 	long	term_supports(long cmp_flags=0);
 	const char* term_type(long term_supports = -1);
 	const char* term_charset(long term_supports = -1);
@@ -971,7 +959,6 @@ public:
 
 	/* logout.cpp */
 	void	logout(void);
-	void	backout(void);
 
 	/* newuser.cpp */
 	BOOL	newuser(void);					/* Get new user							*/
@@ -1036,44 +1023,43 @@ public:
 	bool	bulkupload(uint dirnum);
 
 	/* download.cpp */
-	void	downloadfile(file_t* f);
-	void	notdownloaded(ulong size, time_t start, time_t end);
-	int		protocol(prot_t* prot, enum XFER_TYPE, char *fpath, char *fspec, bool cd, bool autohangup=true);
+	void	downloadedfile(file_t* f);
+	void	notdownloaded(off_t size, time_t start, time_t end);
+	int		protocol(prot_t* prot, enum XFER_TYPE, const char *fpath, const char *fspec, bool cd, bool autohangup=true);
 	const char*	protcmdline(prot_t* prot, enum XFER_TYPE type);
 	void	seqwait(uint devnum);
 	void	autohangup(void);
 	bool	checkdszlog(const char*);
+	bool	checkprotresult(prot_t*, int error, const char* fpath);
 	bool	checkprotresult(prot_t*, int error, file_t*);
+	bool	sendfile(file_t*, char prot, bool autohang);
 	bool	sendfile(char* fname, char prot=0, const char* description = NULL, bool autohang=true);
 	bool	recvfile(char* fname, char prot=0, bool autohang=true);
 
 	/* file.cpp */
-	void	fileinfo(file_t* f);
-	void	openfile(file_t* f);
-	void	closefile(file_t* f);
-	bool	removefcdt(file_t* f);
-	bool	removefile(file_t* f);
-	bool	movefile(file_t* f, int newdir);
+	void	showfileinfo(file_t*, bool show_extdesc = true);
+	bool	removefcdt(file_t*);
+	bool	removefile(smb_t*, file_t*);
+	bool	movefile(smb_t*, file_t*, int newdir);
 	char *	getfilespec(char *str);
 	bool	checkfname(char *fname);
-	bool	addtobatdl(file_t* f);
+	bool	addtobatdl(file_t*);
+	bool	clearbatdl(void);
+	bool	clearbatul(void);
 	long	delfiles(const char *inpath, const char *spec, size_t keep = 0);
 
 	/* listfile.cpp */
-	bool	listfile(const char *fname, const char *buf, uint dirnum
-				,const char *search, const char letter, ulong datoffset);
-	int		listfiles(uint dirnum, const char *filespec, int tofile, long mode);
-	int		listfileinfo(uint dirnum, char *filespec, long mode);
-	void	listfiletofile(char *fname, char *buf, uint dirnum, int file);
-	int		batchflagprompt(uint dirnum, file_t bf[], ulong row[], uint total, long totalfiles);
+	bool	listfile(file_t*, uint dirnum, const char *search, const char letter);
+	int		listfiles(uint dirnum, const char *filespec, FILE* tofile, long mode);
+	int		listfileinfo(uint dirnum, const char *filespec, long mode);
+	void	listfiletofile(file_t*, FILE*);
+	int		batchflagprompt(smb_t*, file_t* bf[], ulong row[], uint total, long totalfiles);
 
 	/* bat_xfer.cpp */
 	void	batchmenu(void);
-	void	batch_create_list(void);
 	void	batch_add_list(char *list);
 	bool	create_batchup_lst(void);
 	bool	create_batchdn_lst(bool native);
-	bool	create_bimodem_pth(void);
 	void	batch_upload(void);
 	void	batch_download(int xfrprot);
 	BOOL	start_batch_download(void);
@@ -1081,17 +1067,14 @@ public:
 	/* tmp_xfer.cpp */
 	void	temp_xfer(void);
 	void	extract(uint dirnum);
-	char *	temp_cmd(void);					/* Returns temp file command line */
+	const char*	temp_cmd(void);					/* Returns temp file command line */
 	ulong	create_filelist(const char *name, long mode);
 
 	/* viewfile.cpp */
-	int		viewfile(file_t* f, int ext);
+	int		viewfile(file_t* f, bool extdesc);
 	void	viewfiles(uint dirnum, char *fspec);
 	void	viewfilecontents(file_t* f);
 
-	/* sortdir.cpp */
-	void	resort(uint dirnum);
-
 	/* xtrn.cpp */
 	int		external(const char* cmdline, long mode, const char* startup_dir=NULL);
 	long	xtrn_mode;
@@ -1108,14 +1091,14 @@ public:
 
 	/* logfile.cpp */
 	void	logentry(const char *code,const char *entry);
-	void	log(char *str);				/* Writes 'str' to node log */
+	void	log(const char *str);				/* Writes 'str' to node log */
 	void	logch(char ch, bool comma);	/* Writes 'ch' to node log */
 	void	logline(const char *code,const char *str); /* Writes 'str' on it's own line in log (LOG_INFO level) */
 	void	logline(int level, const char *code,const char *str);
 	void	logofflist(void);              /* List of users logon activity */
 	bool	errormsg_inside;
 	void	errormsg(int line, const char* function, const char *source, const char* action, const char *object
-				,long access, const char *extinfo=NULL);
+				,long access=0, const char *extinfo=NULL);
 	BOOL	hacklog(char* prot, char* text);
 
 	/* qwk.cpp */
@@ -1233,6 +1216,8 @@ extern "C" {
 	DLLEXPORT char*		DLLCALL ansi_attr(int attr, int curattr, char* str, BOOL color);
 
 	/* main.cpp */
+	extern const char* nulstr;
+	extern const char* crlf;
 	DLLEXPORT int		DLLCALL sbbs_random(int);
 	DLLEXPORT void		DLLCALL sbbs_srand(void);
 
@@ -1261,10 +1246,6 @@ extern "C" {
 	DLLEXPORT int		DLLCALL set_socket_options(scfg_t* cfg, SOCKET sock, const char* section
 		,char* error, size_t errlen);
 
-	/* xtrn.cpp */
-	DLLEXPORT char*		DLLCALL cmdstr(scfg_t* cfg, user_t* user, const char* instr
-									,const char* fpath, const char* fspec, char* cmd);
-
 	/* qwk.cpp */
 	DLLEXPORT int		qwk_route(scfg_t*, const char *inaddr, char *fulladdr, size_t maxlen);
 
@@ -1421,6 +1402,9 @@ extern "C" {
 	DLLEXPORT BOOL		DLLCALL js_ParseMsgHeaderObject(JSContext* cx, JSObject* hdrobj, smbmsg_t*);
 	DLLEXPORT BOOL		DLLCALL js_GetMsgHeaderObjectPrivates(JSContext* cx, JSObject* hdrobj, smb_t**, smbmsg_t**, post_t**);
 
+	/* js_filebase.c */
+	DLLEXPORT JSObject* DLLCALL js_CreateFileBaseClass(JSContext*, JSObject* parent, scfg_t*);
+
 	/* js_socket.c */
 	DLLEXPORT JSObject* DLLCALL js_CreateSocketClass(JSContext* cx, JSObject* parent);
 #ifdef USE_CRYPTLIB
@@ -1451,6 +1435,9 @@ extern "C" {
 	DLLEXPORT JSObject* DLLCALL js_CreateFileClass(JSContext* cx, JSObject* parent);
 	DLLEXPORT JSObject* DLLCALL js_CreateFileObject(JSContext* cx, JSObject* parent, char *name, int fd, const char* mode);
 
+	/* js_archive.c */
+	DLLEXPORT JSObject* DLLCALL js_CreateArchiveClass(JSContext* cx, JSObject* parent);
+
 	/* js_sprintf.c */
 	DLLEXPORT char*		DLLCALL js_sprintf(JSContext* cx, uint argn, unsigned argc, jsval *argv);
 	DLLEXPORT void		DLLCALL js_sprintf_free(char *);
diff --git a/src/sbbs3/sbbs.vcxproj b/src/sbbs3/sbbs.vcxproj
index d71e501bee654dd794ee3e3d579668f54195658f..53cc67719c6543720b091e32d53776e962c695d8 100644
--- a/src/sbbs3/sbbs.vcxproj
+++ b/src/sbbs3/sbbs.vcxproj
@@ -42,6 +42,7 @@
     <Import Project="..\xpdev\xpdev_mt.props" />
     <Import Project="..\encode\encode.props" />
     <Import Project="..\hash\hash.props" />
+    <Import Project="..\..\3rdp\win32.release\libarchive\libarchive.props" />
   </ImportGroup>
   <ImportGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="PropertySheets">
     <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
@@ -56,6 +57,7 @@
     <Import Project="..\xpdev\xpdev_mt.props" />
     <Import Project="..\encode\encode.props" />
     <Import Project="..\hash\hash.props" />
+    <Import Project="..\..\3rdp\win32.release\libarchive\libarchive.props" />
   </ImportGroup>
   <PropertyGroup Label="UserMacros" />
   <PropertyGroup>
@@ -80,7 +82,7 @@
     <ClCompile>
       <Optimization>Disabled</Optimization>
       <AdditionalIncludeDirectories>%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
-      <PreprocessorDefinitions>_DEBUG;WIN32;_WINDOWS;_USRDLL;SBBS;SBBS_EXPORTS;SMB_EXPORTS;RINGBUF_SEM;RINGBUF_MUTEX;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <PreprocessorDefinitions>_DEBUG;WIN32;_WINDOWS;_USRDLL;SBBS;SBBS_EXPORTS;SMB_EXPORTS;RINGBUF_SEM;RINGBUF_EVENT;RINGBUF_MUTEX;%(PreprocessorDefinitions)</PreprocessorDefinitions>
       <BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
       <RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
       <PrecompiledHeaderOutputFile>.\msvc.win32.debug\sbbs/sbbs.pch</PrecompiledHeaderOutputFile>
@@ -132,7 +134,7 @@
       <Optimization>MaxSpeed</Optimization>
       <InlineFunctionExpansion>OnlyExplicitInline</InlineFunctionExpansion>
       <AdditionalIncludeDirectories>%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
-      <PreprocessorDefinitions>NDEBUG;WIN32;_WINDOWS;_USRDLL;SBBS;SBBS_EXPORTS;SMB_EXPORTS;RINGBUF_SEM;RINGBUF_MUTEX;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <PreprocessorDefinitions>NDEBUG;WIN32;_WINDOWS;_USRDLL;SBBS;SBBS_EXPORTS;SMB_EXPORTS;RINGBUF_SEM;RINGBUF_EVENT;RINGBUF_MUTEX;%(PreprocessorDefinitions)</PreprocessorDefinitions>
       <StringPooling>true</StringPooling>
       <RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
       <FunctionLevelLinking>true</FunctionLevelLinking>
@@ -374,6 +376,7 @@
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
     </ClCompile>
+    <ClCompile Include="js_archive.c" />
     <ClCompile Include="js_bbs.cpp">
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
@@ -402,6 +405,7 @@
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
     </ClCompile>
+    <ClCompile Include="js_filebase.c" />
     <ClCompile Include="js_file_area.c">
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
@@ -678,12 +682,6 @@
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
     </ClCompile>
-    <ClCompile Include="sortdir.cpp">
-      <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
-      <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
-      <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
-      <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
-    </ClCompile>
     <ClCompile Include="ssl.c" />
     <ClCompile Include="str.cpp">
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
diff --git a/src/sbbs3/sbbs3.sln b/src/sbbs3/sbbs3.sln
index 7bd8bc1c62ccf891d768a35ad757fcf56e51d7a1..43e18aa81ef7d50afc616de89c52893408fd4691 100644
--- a/src/sbbs3/sbbs3.sln
+++ b/src/sbbs3/sbbs3.sln
@@ -84,6 +84,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "pktdump", "pktdump.vcxproj"
 EndProject
 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "fmsgdump", "fmsgdump.vcxproj", "{E5F4F589-8238-4AE6-8E6D-40AB6D771E1C}"
 EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "upgrade_to_v319", "upgrade_to_v319.vcxproj", "{B84CB739-8425-4612-BDEF-B292BEAE858F}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Win32 = Debug|Win32
@@ -246,6 +248,10 @@ Global
 		{E5F4F589-8238-4AE6-8E6D-40AB6D771E1C}.Debug|Win32.Build.0 = Debug|Win32
 		{E5F4F589-8238-4AE6-8E6D-40AB6D771E1C}.Release|Win32.ActiveCfg = Release|Win32
 		{E5F4F589-8238-4AE6-8E6D-40AB6D771E1C}.Release|Win32.Build.0 = Release|Win32
+		{B84CB739-8425-4612-BDEF-B292BEAE858F}.Debug|Win32.ActiveCfg = Debug|Win32
+		{B84CB739-8425-4612-BDEF-B292BEAE858F}.Debug|Win32.Build.0 = Debug|Win32
+		{B84CB739-8425-4612-BDEF-B292BEAE858F}.Release|Win32.ActiveCfg = Release|Win32
+		{B84CB739-8425-4612-BDEF-B292BEAE858F}.Release|Win32.Build.0 = Release|Win32
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
diff --git a/src/sbbs3/sbbs4defs.h b/src/sbbs3/sbbs4defs.h
index 0b8483974161c90222bdd70d4cc388065efa8c31..b73b0641e492301a89226b4f600fdb43281259f5 100644
--- a/src/sbbs3/sbbs4defs.h
+++ b/src/sbbs3/sbbs4defs.h
@@ -45,6 +45,7 @@ char* user_dat_columns[] = {
 	 "Alias"
 	,"Name"
 	,"Handle"
+	,"Note"
 	,"IpAddress"
 	,"Hostname"
 	,"Comment"
@@ -55,7 +56,7 @@ char* user_dat_columns[] = {
 	,"Netmail"
 	,"Address"
 	,"Location"
-	,"Zip Code"
+	,"ZipCode"
 	,"Password"
 	,"PhoneNumber"
 	,"BirthDate"
diff --git a/src/sbbs3/sbbs_ini.c b/src/sbbs3/sbbs_ini.c
index 760cacd75343438bdca601d6593cf35c4b4ef36e..ab3d89f460d185a50723474975fb6106d6ff9f47 100644
--- a/src/sbbs3/sbbs_ini.c
+++ b/src/sbbs3/sbbs_ini.c
@@ -255,9 +255,11 @@ void sbbs_read_ini(
 {
 	const char*	section;
 	const char* default_term_ansi;
+#if defined(__linux__) || defined(__FreeBSD__)
 	const char*	default_dosemu_path;
 #if defined(__linux__)
 	const char*	default_dosemuconf_path;
+#endif
 #endif
 	char		value[INI_MAX_VALUE_LEN];
 	str_list_t	list;
diff --git a/src/sbbs3/sbbsdefs.h b/src/sbbs3/sbbsdefs.h
index be1fb144f60b3b429f192ae07b45b7d8ae25ffb3..096d7ff1bd45b8a6f60b15683ef35dd087fbaee5 100644
--- a/src/sbbs3/sbbsdefs.h
+++ b/src/sbbs3/sbbsdefs.h
@@ -34,16 +34,14 @@
 /* Constants */
 /*************/
 
-#define VERSION 	"3.18"  /* Version: Major.minor  */
-#define REVISION	'c'     /* Revision: lowercase letter */
-#define VERSION_NUM	(31800	 + (tolower(REVISION)-'a'))
-#define VERSION_HEX	(0x31800 + (tolower(REVISION)-'a'))
+#define VERSION 	"3.19"  /* Version: Major.minor  */
+#define REVISION	'a'     /* Revision: lowercase letter */
+#define VERSION_NUM	(31900	 + (tolower(REVISION)-'a'))
+#define VERSION_HEX	(0x31900 + (tolower(REVISION)-'a'))
 
 #define VERSION_NOTICE		"Synchronet BBS for " PLATFORM_DESC\
 								"  Version " VERSION
-#define SYNCHRONET_CRC		0x9BCDD162
-#define COPYRIGHT_NOTICE	"Copyright 2020 Rob Swindell"
-#define COPYRIGHT_CRC		0xB12E96E6
+#define COPYRIGHT_NOTICE	"Copyright 2021 Rob Swindell"
 
 #define SBBSCTRL_DEFAULT	"/sbbs/ctrl"
 
@@ -51,7 +49,9 @@
 
 #define FNOPEN_BUF_SIZE		(2*1024)
 
+#define MAX_FILENAME_LEN		64
 #define ILLEGAL_FILENAME_CHARS	"\\/|<>:\";,%"
+#define SAFEST_FILENAME_CHARS	"-._0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
 
 #define BIND_FAILURE_HELP	"!Another application or service may be using this port"
 #define UNKNOWN_LOAD_ERROR	"Unknown load error - Library mismatch?"
@@ -86,9 +86,6 @@
 #define MAX_DIRS		65534
 #define MAX_XTRNS		65534
 
-#define MAX_FILES	  10000 /* Maximum number of files per dir			*/
-#define MAX_USERXFER	500 /* Maximum number of dest. users of usrxfer */
-
 #define MAX_TEXTDAT_ITEM_LEN	2000
 
 
@@ -261,10 +258,8 @@
 #define DIR_NOSTAT		(1<<19)		/* Do not include transfers in system stats */
 #define DIR_FILES		(1<<20)		/* List/access files not in database */
 #define DIR_TEMPLATE	(1<<21)		/* Use this dir as template for new dirs (in this lib) */
-
-                                    /* Bit values for file_t.misc */
-#define FM_EXTDESC  (1<<0)          /* Extended description exists */
-#define FM_ANON 	(1<<1)			/* Anonymous upload */
+#define DIR_NOHASH		(1<<22)		/* Don't auto calculate/store file content hashes */
+#define DIR_FILETAGS	(1<<23)		/* Allow files to have user-specified tags */
 
 									/* Bit values for cfg.file_misc				*/
 #define FM_NO_LFN	(1<<0)			/* No long filenames in listings			*/
@@ -293,12 +288,15 @@
 #define ERR_IOCTL	"sending IOCTL"	/* IOCTL error */
 #define ERR_SEEK	"seeking"		/* SEEKing error */
 
-enum {                              /* Values for dir[x].sort */
-     SORT_NAME_A                    /* Sort by filename, ascending */
-    ,SORT_NAME_D                    /* Sort by filename, descending */
-    ,SORT_DATE_A                    /* Sort by upload date, ascending */
-    ,SORT_DATE_D                    /* Sort by upload date, descending */
-    };
+enum file_sort {                    /* Values for dir[x].sort */
+     FILE_SORT_NAME_A   = 0	        /* Sort by filename, ascending (case-insensitive) */
+    ,FILE_SORT_NAME_D   = 1         /* Sort by filename, descending (case-insensitive) */
+	,FILE_SORT_NAME_AC  = 4			/* Sort by filename, ascending (case-sensitive) */
+	,FILE_SORT_NAME_DC  = 5         /* Sort by filename, descending (case-sensitive) */
+    ,FILE_SORT_DATE_A   = 2         /* Sort by upload date, ascending */
+    ,FILE_SORT_DATE_D   = 3         /* Sort by upload date, descending */
+	,FILE_SORT_NATURAL  = FILE_SORT_DATE_A
+};
 
 /* Values for grp[x].sort */
 enum area_sort {
@@ -508,7 +506,7 @@ typedef enum {						/* Values for xtrn_t.event				*/
 #define LEN_ZIPCODE 	10	/* Zip/Postal code								*/
 #define LEN_MODEM		 8	/* User modem type description					*/
 #define LEN_FDESC		58	/* File description 							*/
-#define LEN_FCDT		 9	/* 9 digits for file credit values				*/
+#define LEN_EXTDESC		1024 /* extended file description */
 #define LEN_TITLE		70	/* Message title								*/
 #define LEN_MAIN_CMD	28	/* Unused Storage in user.dat					*/
 #define LEN_COLS		3
@@ -602,23 +600,6 @@ typedef enum {						/* Values for xtrn_t.event				*/
 #define U_UNUSED	U_CURDIR+16
 #define U_LEN		(U_UNUSED+4+2)
 
-/****************************************************************************/
-/* Offsets into DIR .DAT file for different fields for each file 			*/
-/****************************************************************************/
-#define F_CDT		0				/* Offset in DIR#.DAT file for cdts		*/
-#define F_DESC		(F_CDT+LEN_FCDT)/* Description							*/
-#define F_ULER		(F_DESC+LEN_FDESC+2)   /* Uploader						*/
-#define F_TIMESDLED (F_ULER+30+2) 	/* Number of times downloaded 			*/
-#define F_OPENCOUNT	(F_TIMESDLED+5+2)
-#define F_MISC		(F_OPENCOUNT+3+2)
-#define F_ALTPATH	(F_MISC+1)		/* Two hex digit alternate path */
-#define F_LEN		(F_ALTPATH+2+2) /* Total length of all fdat in file		*/
-
-#define F_IXBSIZE	22				/* Length of each index entry			*/
-
-#define F_EXBSIZE	512				/* Length of each ext-desc entry		*/
-
-
 #define SIF_MAXBUF  0x7000			/* Maximum buffer size of SIF data		*/
 
 /* NOTE: Do not change the values of the following block of defines!		*/
@@ -881,8 +862,6 @@ enum {							/* Values for 'mode' in listfileinfo        */
 	,FI_OLD              		/* Search/Remove files not downloaded since */
 	,FI_OLDUL	 				/* Search/Remove files uploaded before      */
 	,FI_OFFLINE   				/* Search/Remove files not online			*/
-	,FI_USERXFER  				/* User Xfer Download                       */
-	,FI_CLOSE 	  				/* Close any open records					*/
 	};
 
 enum XFER_TYPE {				/* Values for type in xfer_prot_select()	*/
@@ -890,7 +869,6 @@ enum XFER_TYPE {				/* Values for type in xfer_prot_select()	*/
 	,XFER_DOWNLOAD
 	,XFER_BATCH_UPLOAD
 	,XFER_BATCH_DOWNLOAD
-	,XFER_BIDIR
 };
 
 #define L_LOGON     1			/* Logon List maintenance                   */
@@ -918,11 +896,6 @@ enum {							/* Values of mode for userlist function     */
 	,UL_DIR						/* List all users with access to curdir 	*/
 	};
 
-
-#define BO_LEN		16			/* backout.dab record length				*/
-
-#define BO_OPENFILE 0			/* Backout types */
-
 /**********/
 /* Macros */
 /**********/
@@ -1060,25 +1033,6 @@ typedef struct {						/* Users information */
 
 } user_t;
 
-typedef struct {						/* File (transfers) Data */
-	char    name[13],					/* Name of file FILENAME.EXT */
-			desc[LEN_FDESC+1],			/* Uploader's Description */
-			uler[LEN_ALIAS+1];			/* User who uploaded */
-	uchar	opencount;					/* Times record is currently open */
-	time32_t  date,						/* File date/time */
-			dateuled,					/* Date/Time (Unix) Uploaded */
-			datedled;					/* Date/Time (Unix) Last downloaded */
-	uint16_t	dir,						/* Directory file is in */
-			altpath,
-			timesdled,					/* Total times downloaded */
-			timetodl;					/* How long transfer time */
-	int32_t	datoffset,					/* Offset into .DAT file */
-			size,						/* Size of file */
-			misc;						/* Miscellaneous bits */
-	uint32_t	cdt;						/* Credit value for this file */
-
-} file_t;
-
 typedef struct {
 	idxrec_t	idx;					/* defined in smbdefs.h */
 	uint32_t	num;					/* 1-based offset */
@@ -1093,6 +1047,7 @@ typedef struct {
 } post_t;
 typedef idxrec_t mail_t;				/* defined in smbdefs.h */
 typedef fidoaddr_t faddr_t;				/* defined in smbdefs.h */
+typedef smbfile_t file_t;				/* defined in smbdefs.h */
 
 typedef struct {						/* System/Node Statistics */
 	uint32_t	logons,						/* Total Logons on System */
diff --git a/src/sbbs3/sbbsecho.c b/src/sbbs3/sbbsecho.c
index 9cf5b05c44bf95e85598f257366cb8dac22c905c..80c340f51a0b88de61a06d7e0cb8132ac3ac323e 100644
--- a/src/sbbs3/sbbsecho.c
+++ b/src/sbbs3/sbbsecho.c
@@ -50,10 +50,12 @@
 #include "nopen.h"
 #include "crc32.h"
 #include "userdat.h"
+#include "filedat.h"
 #include "msg_id.h"
 #include "scfgsave.h"
 #include "getmail.h"
-#include "ver.h"
+#include "git_branch.h"
+#include "git_hash.h"
 
 #define MAX_OPEN_SMBS	10
 
@@ -117,7 +119,7 @@ const char* sbbsecho_pid(void)
 	static char str[256];
 
 	sprintf(str, "SBBSecho %u.%02u-%s %s/%s %s %s"
-		,SBBSECHO_VERSION_MAJOR,SBBSECHO_VERSION_MINOR,PLATFORM_DESC,git_branch,git_hash,__DATE__,compiler);
+		,SBBSECHO_VERSION_MAJOR,SBBSECHO_VERSION_MINOR,PLATFORM_DESC,GIT_BRANCH,GIT_HASH,__DATE__,compiler);
 
 	return str;
 }
@@ -206,7 +208,7 @@ int fwrite_via_control_line(FILE* fp, fidoaddr_t* addr)
 		,tm->tm_hour
 		,tm->tm_min
 		,tm->tm_sec
-		,SBBSECHO_VERSION_MAJOR,SBBSECHO_VERSION_MINOR,PLATFORM_DESC,git_branch,git_hash);
+		,SBBSECHO_VERSION_MAJOR,SBBSECHO_VERSION_MINOR,PLATFORM_DESC,GIT_BRANCH,GIT_HASH);
 }
 
 int fwrite_intl_control_line(FILE* fp, fmsghdr_t* hdr)
@@ -555,121 +557,6 @@ bool delfile(const char *filename, int line)
 	return true;
 }
 
-/*****************************************************************************/
-/* Returns command line generated from instr with %c replacments             */
-/*****************************************************************************/
-char *mycmdstr(scfg_t* cfg, const char *instr, const char *fpath, const char *fspec)
-{
-    static char cmd[MAX_PATH+1];
-    char str[256],str2[128];
-    int i,j,len;
-
-	len=strlen(instr);
-	for(i=j=0;i<len && j<128;i++) {
-		if(instr[i]=='%') {
-			i++;
-			cmd[j]=0;
-			switch(toupper(instr[i])) {
-				case 'F':   /* File path */
-					SAFECAT(cmd,fpath);
-					break;
-				case 'G':   /* Temp directory */
-					if(cfg->temp_dir[0]!='\\'
-						&& cfg->temp_dir[0]!='/'
-						&& cfg->temp_dir[1]!=':') {
-						SAFECOPY(str,cfg->node_dir);
-						SAFECAT(str,cfg->temp_dir);
-						if(FULLPATH(str2,str,40))
-							strcpy(str,str2);
-						backslash(str);
-						SAFECAT(cmd,str);}
-					else
-						SAFECAT(cmd,cfg->temp_dir);
-					break;
-				case 'J':
-					if(cfg->data_dir[0]!='\\'
-						&& cfg->data_dir[0]!='/'
-						&& cfg->data_dir[1]!=':') {
-						SAFECOPY(str,cfg->node_dir);
-						SAFECAT(str,cfg->data_dir);
-						if(FULLPATH(str2,str,40))
-							SAFECOPY(str,str2);
-						backslash(str);
-						SAFECAT(cmd,str);
-					}
-					else
-						SAFECAT(cmd,cfg->data_dir);
-					break;
-				case 'K':
-					if(cfg->ctrl_dir[0]!='\\'
-						&& cfg->ctrl_dir[0]!='/'
-						&& cfg->ctrl_dir[1]!=':') {
-						SAFECOPY(str,cfg->node_dir);
-						SAFECAT(str,cfg->ctrl_dir);
-						if(FULLPATH(str2,str,40))
-							SAFECOPY(str,str2);
-						backslash(str);
-						SAFECAT(cmd,str);
-					}
-					else
-						SAFECAT(cmd,cfg->ctrl_dir);
-					break;
-				case 'N':   /* Node Directory (same as SBBSNODE environment var) */
-					SAFECAT(cmd,cfg->node_dir);
-					break;
-				case 'O':   /* SysOp */
-					SAFECAT(cmd,cfg->sys_op);
-					break;
-				case 'Q':   /* QWK ID */
-					SAFECAT(cmd,cfg->sys_id);
-					break;
-				case 'S':   /* File Spec */
-					SAFECAT(cmd,fspec);
-					break;
-				case '!':   /* EXEC Directory */
-					SAFECAT(cmd,cfg->exec_dir);
-					break;
-                case '@':   /* EXEC Directory for DOS/OS2/Win32, blank for Unix */
-#ifndef __unix__
-                    SAFECAT(cmd,cfg->exec_dir);
-#endif
-                    break;
-				case '#':   /* Node number (same as SBBSNNUM environment var) */
-					sprintf(str,"%d",cfg->node_num);
-					SAFECAT(cmd,str);
-					break;
-				case '*':
-					sprintf(str,"%03d",cfg->node_num);
-					SAFECAT(cmd,str);
-					break;
-				case '%':   /* %% for percent sign */
-					SAFECAT(cmd,"%");
-					break;
-				case '.':	/* .exe for DOS/OS2/Win32, blank for Unix */
-#ifndef __unix__
-					SAFECAT(cmd,".exe");
-#endif
-					break;
-				case '?':	/* Platform */
-					SAFECOPY(str,PLATFORM_DESC);
-					strlwr(str);
-					SAFECAT(cmd,str);
-					break;
-				default:    /* unknown specification */
-					lprintf(LOG_ERR,"ERROR Checking Command Line '%s'",instr);
-					bail(1);
-					break;
-			}
-			j=strlen(cmd);
-		}
-		else
-			cmd[j++]=instr[i];
-}
-	cmd[j]=0;
-
-	return(cmd);
-}
-
 /****************************************************************************/
 /* Runs an external program directly using system()							*/
 /****************************************************************************/
@@ -2373,9 +2260,21 @@ char* process_areafix(fidoaddr_t addr, char* inbuf, const char* password, const
 int unpack(const char *infile, const char* outdir)
 {
 	FILE *stream;
-	char str[256],tmp[128];
+	char str[256],tmp[512];
+	char error[256];
 	int ch,file;
 	unsigned u,j;
+	long file_count;
+
+	file_count = extract_files_from_archive(infile, outdir
+		,/* allowed_filename_chars: */SAFEST_FILENAME_CHARS, /* with_path */false
+		,/* max_files: */0, /* file_list = ALL */NULL, error, sizeof(error));
+	if(file_count > 0) {
+		lprintf(LOG_DEBUG, "libarchive extracted %lu files from %s", file_count, infile);
+		return 0;
+	}
+	if(*error)
+		lprintf(LOG_NOTICE, "libarchive error (%s) extracting %s", error, infile);
 
 	if((stream=fnopen(&file,infile,O_RDONLY))==NULL) {
 		lprintf(LOG_ERR,"ERROR %u (%s) opening archive: %s",errno,strerror(errno),infile);
@@ -2405,7 +2304,7 @@ int unpack(const char *infile, const char* outdir)
 		return(1);
 	}
 
-	return execute(mycmdstr(&scfg,cfg.arcdef[u].unpack,infile, outdir));
+	return execute(cmdstr(&scfg, /* user: */NULL, cfg.arcdef[u].unpack,infile, outdir, tmp, sizeof(tmp)));
 }
 
 /******************************************************************************
@@ -2417,6 +2316,7 @@ int pack(const char *srcfile, const char *destfile, fidoaddr_t dest)
 {
 	nodecfg_t*	nodecfg;
 	arcdef_t*	archive;
+	char tmp[512];
 
 	if(!cfg.arcdefs) {
 		lprintf(LOG_DEBUG, "ERROR: pack() called with no archive types configured!");
@@ -2429,7 +2329,13 @@ int pack(const char *srcfile, const char *destfile, fidoaddr_t dest)
 
 	lprintf(LOG_DEBUG,"Packing packet (%s) into bundle (%s) for %s using %s"
 		,srcfile, destfile, smb_faddrtoa(&dest, NULL), archive->name);
-	return execute(mycmdstr(&scfg, archive->pack, destfile, srcfile));
+	if(strListFind((str_list_t)supported_archive_formats, archive->name, /* case_sensitive */FALSE) >= 0) {
+		const char* file_list[] = { srcfile, NULL };
+		if(create_archive(destfile, archive->name, /* with_path: */false, (str_list_t)file_list, tmp, sizeof(tmp)) == 1)
+			return 0;
+		lprintf(LOG_ERR, "libarchive error (%s) creating %s", tmp, destfile);
+	}
+	return execute(cmdstr(&scfg, /* user: */NULL, archive->pack, destfile, srcfile, tmp, sizeof(tmp)));
 }
 
 /* Reads a single FTS-1 stored message header from the specified file stream and terminates C-strings */
@@ -2824,7 +2730,8 @@ int mv(const char *insrc, const char *indest, bool copy)
 	char src[MAX_PATH+1];
 	char dest[MAX_PATH+1];
 	int  ind,outd;
-	long length,chunk=4096,l;
+	off_t length;
+	long chunk=4096,l;
     FILE *inp,*outp;
 
 	SAFECOPY(src, insrc);
@@ -2881,7 +2788,7 @@ int mv(const char *insrc, const char *indest, bool copy)
 	int result = 0;
 	while(l<length) {
 		if(l+chunk>length)
-			chunk=length-l;
+			chunk=(long)(length-l);
 		if(fread(buf,1,chunk,inp) != chunk) {
 			result = -2;
 			break;
@@ -2932,7 +2839,7 @@ long getlastmsg(uint subnum, uint32_t *ptr, /* unused: */time_t *t)
 ulong loadmsgs(smb_t* smb, post_t** post, ulong ptr)
 {
 	int i;
-	long l,total;
+	ulong l,total;
 	idxrec_t idx;
 
 	if((i=smb_locksmbhdr(smb))!=SMB_SUCCESS) {
@@ -2941,14 +2848,14 @@ ulong loadmsgs(smb_t* smb, post_t** post, ulong ptr)
 	}
 
 	/* total msgs in sub */
-	total=filelength(fileno(smb->sid_fp))/sizeof(idxrec_t);
+	total=(ulong)filelength(fileno(smb->sid_fp))/sizeof(idxrec_t);
 
 	if(!total) {			/* empty */
 		smb_unlocksmbhdr(smb);
 		return(0);
 	}
 
-	if(((*post)=(post_t*)malloc(sizeof(post_t)*total))    /* alloc for max */
+	if(((*post)=(post_t*)malloc((size_t)(sizeof(post_t)*total)))    /* alloc for max */
 		==NULL) {
 		smb_unlocksmbhdr(smb);
 		lprintf(LOG_ERR,"ERROR line %d allocating %lu bytes for %s",__LINE__
@@ -3276,6 +3183,7 @@ int fmsgtosmsg(char* fbuf, fmsghdr_t* hdr, uint usernumber, uint subnum)
 {
 	uchar	ch,stail[MAX_TAILLEN+1],*sbody;
 	char	msg_id[256],str[128],*p;
+	char	cmd[512];
 	bool	done,cr;
 	int 	i;
 	ushort	xlat=XLAT_NONE,net;
@@ -3652,7 +3560,7 @@ int fmsgtosmsg(char* fbuf, fmsghdr_t* hdr, uint usernumber, uint subnum)
 	i=smb_addmsg(smbfile, &msg, smb_storage_mode(&scfg, smbfile), dupechk_hashes, xlat, sbody, stail);
 	if(i == SMB_SUCCESS) {
 		if(subnum != INVALID_SUB && scfg.sub[subnum]->post_sem[0])
-			ftouch(mycmdstr(&scfg, scfg.sub[subnum]->post_sem, "", ""));
+			ftouch(cmdstr(&scfg, /* user: */NULL, scfg.sub[subnum]->post_sem, "", "", cmd, sizeof(cmd)));
 	} else {
 		lprintf(LOG_ERR,"ERROR %d (%s) line %d adding message to %s"
 			,i, smbfile->last_error, __LINE__, subnum==INVALID_SUB ? "mail":scfg.sub[subnum]->code);
@@ -5281,7 +5189,7 @@ int export_netmail(void)
 
 	memset(&msg, 0, sizeof(msg));
 	rewind(email->sid_fp);
-	for(;!feof(email->sid_fp);msg.offset++) {
+	for(;!feof(email->sid_fp);msg.idx_offset++) {
 
 		smb_freemsgmem(&msg);
 		if(fread(&msg.idx, sizeof(msg.idx), 1, email->sid_fp) != 1)
@@ -5360,7 +5268,7 @@ int export_netmail(void)
 			if((i = smb_updatemsg(email, &msg)) != SMB_SUCCESS)
 				lprintf(LOG_ERR,"!ERROR %d (%s) line %d deleting mail msg #%u"
 					,i, email->last_error, __LINE__, msg.hdr.number);
-			(void)fseek(email->sid_fp, (msg.offset+1)*sizeof(msg.idx), SEEK_SET);
+			(void)fseek(email->sid_fp, (msg.idx_offset+1)*sizeof(msg.idx), SEEK_SET);
 		} else {
 			if((i = smb_putmsghdr(email, &msg)) != SMB_SUCCESS)
 				lprintf(LOG_ERR,"!ERROR %d (%s) line %d updating msg header for mail msg #%u"
@@ -5647,7 +5555,7 @@ void find_stray_packets(void)
 	FILE*	fp;
 	size_t	f;
 	glob_t	g;
-	long	flen;
+	off_t	flen;
 	uint16_t	terminator;
 	fidoaddr_t	pkt_orig;
 	fidoaddr_t	pkt_dest;
@@ -5683,7 +5591,7 @@ void find_stray_packets(void)
 			continue;
 		}
 		terminator = ~FIDO_PACKET_TERMINATOR;
-		(void)fseek(fp, flen-sizeof(terminator), SEEK_SET);
+		(void)fseek(fp, (long)(flen-sizeof(terminator)), SEEK_SET);
 		if(fread(&terminator, sizeof(terminator), 1, fp) != 1)
 			lprintf(LOG_WARNING, "Failure reading last byte from %s", packet);
 		fclose(fp);
@@ -6180,7 +6088,7 @@ int main(int argc, char **argv)
 	printf("\nSBBSecho v%u.%02u-%s (%s/%s) - Synchronet FidoNet EchoMail Tosser\n"
 		,SBBSECHO_VERSION_MAJOR, SBBSECHO_VERSION_MINOR
 		,PLATFORM_DESC
-		,git_branch, git_hash
+		,GIT_BRANCH, GIT_HASH
 		);
 
 	cmdline[0]=0;
diff --git a/src/sbbs3/sbbsecho.vcxproj b/src/sbbs3/sbbsecho.vcxproj
index d78e6e0ad913ae0169c2b043cdc79db4b29eba20..179567e9b47812dc33707437cc6aa57642260521 100644
--- a/src/sbbs3/sbbsecho.vcxproj
+++ b/src/sbbs3/sbbsecho.vcxproj
@@ -38,6 +38,7 @@
     <Import Project="..\build\undeprecate.props" />
     <Import Project="..\hash\hash.props" />
     <Import Project="..\encode\encode.props" />
+    <Import Project="..\..\3rdp\win32.release\libarchive\libarchive.props" />
   </ImportGroup>
   <ImportGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="PropertySheets">
     <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
@@ -48,6 +49,7 @@
     <Import Project="..\build\undeprecate.props" />
     <Import Project="..\hash\hash.props" />
     <Import Project="..\encode\encode.props" />
+    <Import Project="..\..\3rdp\win32.release\libarchive\libarchive.props" />
   </ImportGroup>
   <PropertyGroup Label="UserMacros" />
   <PropertyGroup>
@@ -161,6 +163,7 @@
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
     </ClCompile>
+    <ClCompile Include="filedat.c" />
     <ClCompile Include="getmail.c" />
     <ClCompile Include="msg_id.c">
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
@@ -187,7 +190,6 @@
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
     </ClCompile>
-    <ClCompile Include="ver.cpp" />
   </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\smblib\smblib.vcxproj">
diff --git a/src/sbbs3/scandirs.cpp b/src/sbbs3/scandirs.cpp
index 9fd93a675c18f95ba758eec5fa290193068dd277..4c7ecea31603ea32d560a4685ab763fbb0cdc456 100644
--- a/src/sbbs3/scandirs.cpp
+++ b/src/sbbs3/scandirs.cpp
@@ -27,7 +27,6 @@
 void sbbs_t::scandirs(long mode)
 {
 	char	ch,str[256]="";
-	char 	tmp[512];
 	int		s;
 	uint	i,k;
 
@@ -43,9 +42,8 @@ void sbbs_t::scandirs(long mode)
 			bprintf(text[NScanHdr],timestr(ns_time));
 		}
 		else if(mode==FL_NO_HDR) {		/* Search for a string */
-			if(!getfilespec(tmp))
+			if(!getfilespec(str))
 				return;
-			padfname(tmp,str); 
 		}
 		else if(mode==FL_FINDDESC) {	/* Find text in description */
 			if(text[SearchExtendedQ][0] && !noyes(text[SearchExtendedQ]))
@@ -101,7 +99,6 @@ void sbbs_t::scandirs(long mode)
 void sbbs_t::scanalldirs(long mode)
 {
 	char	str[256]="";
-	char 	tmp[512];
 	int		s;
 	uint	i,j,k,d;
 
@@ -111,9 +108,8 @@ void sbbs_t::scanalldirs(long mode)
 		bprintf(text[NScanHdr],timestr(ns_time));
 	}
 	else if(mode==FL_NO_HDR) {		/* Search for a string */
-		if(!getfilespec(tmp))
+		if(!getfilespec(str))
 			return;
-		padfname(tmp,str); 
 	}
 	else if(mode==FL_FINDDESC) {	/* Find text in description */
 		if(text[SearchExtendedQ][0] && !noyes(text[SearchExtendedQ]))
diff --git a/src/sbbs3/scfg/scfg.c b/src/sbbs3/scfg/scfg.c
index cc83f3cdc0f407cd70667e523cbaab6fa5a6abbd..0ee5b9cdec8036e931bbcc97249b24b013ed025f 100644
--- a/src/sbbs3/scfg/scfg.c
+++ b/src/sbbs3/scfg/scfg.c
@@ -45,6 +45,9 @@ char error[256];
 int  backup_level=5;
 char* area_sort_desc[] = { "Index Position", "Long Name", "Short Name", "Internal Code", NULL };
 
+/* convenient space-saving global constants */
+const char* nulstr="";
+
 char *invalid_code=
 	"`Invalid Internal Code:`\n\n"
 	"Internal codes can be up to eight characters in length and can only\n"
diff --git a/src/sbbs3/scfg/scfg.h b/src/sbbs3/scfg/scfg.h
index f2e04f2b10954bcc58ca252689fff836f1bb745d..bfbd9d0dd3338e2a3fb43f4ee2e466e5aa942d91 100644
--- a/src/sbbs3/scfg/scfg.h
+++ b/src/sbbs3/scfg/scfg.h
@@ -100,7 +100,7 @@ extern char item;
 extern char **opt;
 extern char tmp[256];
 extern char error[256];
-extern char *nulstr;
+extern const char *nulstr;
 extern char *invalid_code,*num_flags;
 extern int	backup_level;
 extern BOOL new_install;
diff --git a/src/sbbs3/scfg/scfg.vcxproj b/src/sbbs3/scfg/scfg.vcxproj
index 04cce09ac02cb8996007aecd3fa6acdd8eb06919..84c7072cbbed94ac1e13e418bea6f4d35f8c5c76 100644
--- a/src/sbbs3/scfg/scfg.vcxproj
+++ b/src/sbbs3/scfg/scfg.vcxproj
@@ -41,6 +41,7 @@
     <Import Project="..\..\hash\hash.props" />
     <Import Project="..\..\encode\encode.props" />
     <Import Project="..\..\..\3rdp\win32.release\cryptlib\cryptlib.props" />
+    <Import Project="..\..\..\3rdp\win32.release\libarchive\libarchive.props" />
   </ImportGroup>
   <ImportGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="PropertySheets">
     <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
@@ -54,6 +55,7 @@
     <Import Project="..\..\hash\hash.props" />
     <Import Project="..\..\encode\encode.props" />
     <Import Project="..\..\..\3rdp\win32.release\cryptlib\cryptlib.props" />
+    <Import Project="..\..\..\3rdp\win32.release\libarchive\libarchive.props" />
   </ImportGroup>
   <PropertyGroup Label="UserMacros" />
   <PropertyGroup>
diff --git a/src/sbbs3/scfg/scfgnet.c b/src/sbbs3/scfg/scfgnet.c
index f88d02e874408a753cba5e9424b1af48da4a6416..360a8a32619c8b38473ac0c281b01eefaa3163de 100644
--- a/src/sbbs3/scfg/scfgnet.c
+++ b/src/sbbs3/scfg/scfgnet.c
@@ -193,7 +193,7 @@ void net_cfg()
 							"networked sub-boards. This default can be overridden on a per sub-board\n"
 							"basis with the sub-board configuration `Network Options...`.\n"
 						;
-						uifc.input(WIN_MID|WIN_SAV,0,0,nulstr
+						uifc.input(WIN_MID|WIN_SAV,0,0,""
 							,cfg.qnet_tagline,sizeof(cfg.qnet_tagline)-1,K_MSG|K_EDIT);
 						break;
 					case 0:
@@ -238,8 +238,7 @@ void net_cfg()
 								if(!new_qhub(i))
 									continue;
 								SAFECOPY(cfg.qhub[i]->id,str);
-								SAFECOPY(cfg.qhub[i]->pack,"%@zip -jD %f %s");
-								SAFECOPY(cfg.qhub[i]->unpack,"%@unzip -Coj %f %s -d %g");
+								SAFECOPY(cfg.qhub[i]->fmt, "ZIP");
 								SAFECOPY(cfg.qhub[i]->call,"*qnet-ftp %s hub.address YOURPASS");
 								cfg.qhub[i]->node = NODE_ANY;
 								cfg.qhub[i]->days=0x7f; /* all days */
@@ -824,6 +823,7 @@ void qhub_edit(int num)
 	while(!done) {
 		i=0;
 		sprintf(opt[i++],"%-27.27s%s","Hub System ID",cfg.qhub[num]->id);
+		sprintf(opt[i++],"%-27.27s%s","Archive Format",cfg.qhub[num]->fmt);
 		sprintf(opt[i++],"%-27.27s%s","Pack Command Line",cfg.qhub[num]->pack);
 		sprintf(opt[i++],"%-27.27s%s","Unpack Command Line",cfg.qhub[num]->unpack);
 		sprintf(opt[i++],"%-27.27s%s","Call-out Command Line",cfg.qhub[num]->call);
@@ -859,8 +859,13 @@ void qhub_edit(int num)
 			"\n"
 			"The `Hub System ID` must match the QWK System ID of this network hub.\n"
 			"\n"
+			"The `Archive Format` should be set to an archive/compression format\n"
+			"that the hub will expect your REP packets to be submitted with\n"
+			"(typically, ZIP).\n"
+			"\n"
 			"The `Pack` and `Unpack Command Lines` are used for creating and extracting\n"
-			"REP (reply) and QWK message packets (files, usually in PKZIP format).\n"
+			"REP (reply) and QWK message packets using an external archive utility\n"
+			"(these command-lines are optional).\n"
 			"\n"
 			"The `Call-out Command Line` is executed when your system attempts a packet\n"
 			"exchange with the QWKnet hub (e.g. executes a script).\n"
@@ -904,7 +909,7 @@ void qhub_edit(int num)
 			case -1:
 				done=1;
 				break;
-			case 0:
+			case __COUNTER__:
 				uifc.helpbuf=
 					"`QWK Network Hub System ID:`\n"
 					"\n"
@@ -916,7 +921,17 @@ void qhub_edit(int num)
 					,cfg.qhub[num]->id,LEN_QWKID,K_UPPER|K_EDIT))
 					strcpy(cfg.qhub[num]->id,str);
 				break;
-			case 1:
+			case __COUNTER__:
+				uifc.helpbuf=
+					"`REP Packet Archive Format:`\n"
+					"\n"
+					"This is the archive format used for REP packets created for this QWK\n"
+					"network hub (typically, this would be `ZIP`).\n"
+				;
+				uifc.input(WIN_MID|WIN_SAV,0,0,"REP Packet Archive Format"
+					,cfg.qhub[num]->fmt,sizeof(cfg.qhub[num]->fmt)-1,K_EDIT|K_UPPER);
+				break;
+			case __COUNTER__:
 				uifc.helpbuf=
 					"`REP Packet Creation Command:`\n"
 					"\n"
@@ -928,7 +943,7 @@ void qhub_edit(int num)
 				uifc.input(WIN_MID|WIN_SAV,0,0,""
 					,cfg.qhub[num]->pack,sizeof(cfg.qhub[num]->pack)-1,K_EDIT);
 				break;
-			case 2:
+			case __COUNTER__:
 				uifc.helpbuf=
 					"`QWK Packet Extraction Command:`\n"
 					"\n"
@@ -940,7 +955,7 @@ void qhub_edit(int num)
 				uifc.input(WIN_MID|WIN_SAV,0,0,""
 					,cfg.qhub[num]->unpack,sizeof(cfg.qhub[num]->unpack)-1,K_EDIT);
 				break;
-			case 3:
+			case __COUNTER__:
 				uifc.helpbuf=
 					"`QWK Network Hub Call-out Command Line:`\n"
 					"\n"
@@ -952,7 +967,7 @@ void qhub_edit(int num)
 				uifc.input(WIN_MID|WIN_SAV,0,0,""
 					,cfg.qhub[num]->call,sizeof(cfg.qhub[num]->call)-1,K_EDIT);
 				break;
-			case 4:
+			case __COUNTER__:
 				if(cfg.qhub[num]->node == NODE_ANY)
 					SAFECOPY(str, "Any");
 				else
@@ -971,7 +986,7 @@ void qhub_edit(int num)
 						cfg.qhub[num]->node = NODE_ANY;
 				}
 				break;
-			case 5:
+			case __COUNTER__:
 				j=0;
 				while(1) {
 					for(i=0;i<7;i++)
@@ -992,7 +1007,7 @@ void qhub_edit(int num)
 					uifc.changes=1; 
 				}
 				break;
-			case 6:
+			case __COUNTER__:
 				i=1;
 				uifc.helpbuf=
 					"`Perform Call-out at a Specific Time:`\n"
@@ -1046,27 +1061,27 @@ void qhub_edit(int num)
 					} 
 				}
 				break;
-			case 7:
+			case __COUNTER__:
 				cfg.qhub[num]->misc^=QHUB_NOKLUDGES;
 				uifc.changes=1;
 				break;
-			case 8:
+			case __COUNTER__:
 				cfg.qhub[num]->misc^=QHUB_NOVOTING;
 				uifc.changes=1;
 				break;
-			case 9:
+			case __COUNTER__:
 				cfg.qhub[num]->misc^=QHUB_NOHEADERS;
 				uifc.changes=1;
 				break;
-			case 10:
+			case __COUNTER__:
 				cfg.qhub[num]->misc^=QHUB_UTF8;
 				uifc.changes=1;
 				break;
-			case 11:
+			case __COUNTER__:
 				cfg.qhub[num]->misc^=QHUB_EXT;
 				uifc.changes=1;
 				break;
-			case 12:
+			case __COUNTER__:
 				i = cfg.qhub[num]->misc&QHUB_CTRL_A;
 				i++;
 				if(i == QHUB_CTRL_A) i = 0;
@@ -1074,10 +1089,10 @@ void qhub_edit(int num)
 				cfg.qhub[num]->misc |= i;
 				uifc.changes=1;
 				break;
-			case 13:
+			case __COUNTER__:
 				import_qwk_conferences(num);
 				break;
-			case 14:
+			case __COUNTER__:
 				qhub_sub_edit(num);
 				break; 
 		} 
diff --git a/src/sbbs3/scfg/scfgsub.c b/src/sbbs3/scfg/scfgsub.c
index 540c13bad2c8e2e6001140d26219b678a6ef2082..457dfbe5469c08ce55aaaad50171a1aa3060b2af 100644
--- a/src/sbbs3/scfg/scfgsub.c
+++ b/src/sbbs3/scfg/scfgsub.c
@@ -1305,7 +1305,7 @@ void sub_cfg(uint grpnum)
 									"tagline in the `Networks` configuration, you should enter that tagline\n"
 									"here. If this option is left blank, the default tagline is used.\n"
 								;
-								uifc.input(WIN_MID|WIN_SAV,0,0,nulstr,cfg.sub[i]->tagline
+								uifc.input(WIN_MID|WIN_SAV,0,0,"",cfg.sub[i]->tagline
 									,sizeof(cfg.sub[i]->tagline)-1,K_MSG|K_EDIT);
 								break;
 							case 5:
@@ -1390,7 +1390,7 @@ void sub_cfg(uint grpnum)
 									"\n"
 									"If this option is blank, the default origin line is used.\n"
 								;
-								uifc.input(WIN_MID|WIN_SAV,0,0,nulstr,cfg.sub[i]->origline
+								uifc.input(WIN_MID|WIN_SAV,0,0,"",cfg.sub[i]->origline
 									,sizeof(cfg.sub[i]->origline)-1,K_EDIT);
 								break;
 						} 
diff --git a/src/sbbs3/scfg/scfgxfr1.c b/src/sbbs3/scfg/scfgxfr1.c
index 60747c3b9290576ee0b25c3a40d51888b0a6edc0..098c8d7fbe94f325f9322242f324bd558ca114a4 100644
--- a/src/sbbs3/scfg/scfgxfr1.c
+++ b/src/sbbs3/scfg/scfgxfr1.c
@@ -42,15 +42,12 @@ void xfer_opts()
 	static int dlevent_dflt;
 	static int dlevent_bar;
 	static int dlevent_opt;
-	static int altpath_dflt;
-	static int altpath_bar;
 	static fextr_t savfextr;
 	static fview_t savfview;
 	static ftest_t savftest;
 	static fcomp_t savfcomp;
 	static prot_t savprot;
 	static dlevent_t savdlevent;
-	static char savaltpath[LEN_DIR+1];
 
 	while(1) {
 		i=0;
@@ -60,8 +57,6 @@ void xfer_opts()
 			,cfg.max_batup);
 		sprintf(opt[i++],"%-33.33s%u","Max Files in Batch DL Queue"
 			,cfg.max_batdn);
-		sprintf(opt[i++],"%-33.33s%u","Max Users in User Transfers"
-			,cfg.max_userxfer);
 		sprintf(opt[i++],"%-33.33s%u%%","Default Credit on Upload"
 			,cfg.cdt_up_pct);
 		sprintf(opt[i++],"%-33.33s%u%%","Default Credit on Download"
@@ -71,16 +66,13 @@ void xfer_opts()
 				,cfg.leech_pct,cfg.leech_sec);
 		else
 			strcpy(str,"Disabled");
-		sprintf(opt[i++],"%-33.33s%s","Long Filenames in Listings"
-			,cfg.file_misc&FM_NO_LFN ? "No":"Yes");
 		sprintf(opt[i++],"%-33.33s%s","Leech Protocol Detection",str);
 		strcpy(opt[i++],"Viewable Files...");
 		strcpy(opt[i++],"Testable Files...");
 		strcpy(opt[i++],"Download Events...");
 		strcpy(opt[i++],"Extractable Files...");
-		strcpy(opt[i++],"Compressable Files...");
+		strcpy(opt[i++],"Compressible Files...");
 		strcpy(opt[i++],"Transfer Protocols...");
-		strcpy(opt[i++],"Alternate File Paths...");
 		opt[i][0]=0;
 		uifc.helpbuf=
 			"`File Transfer Configuration:`\n"
@@ -99,7 +91,7 @@ void xfer_opts()
 					refresh_cfg(&cfg);
 				}
 				return;
-			case 0:
+			case __COUNTER__:
 				uifc.helpbuf=
 					"`Minimum Kilobytes Free Disk Space to Allow Uploads:`\n"
 					"\n"
@@ -111,7 +103,7 @@ void xfer_opts()
 					,ultoa(cfg.min_dspace,tmp,10),5,K_EDIT|K_NUMBER);
 				cfg.min_dspace=atoi(tmp);
 				break;
-			case 1:
+			case __COUNTER__:
 				uifc.helpbuf=
 					"`Maximum Files in Batch Upload Queue:`\n"
 					"\n"
@@ -122,7 +114,7 @@ void xfer_opts()
 					,ultoa(cfg.max_batup,tmp,10),5,K_EDIT|K_NUMBER);
 				cfg.max_batup=atoi(tmp);
 				break;
-			case 2:
+			case __COUNTER__:
 				uifc.helpbuf=
 					"`Maximum Files in Batch Download Queue:`\n"
 					"\n"
@@ -133,19 +125,7 @@ void xfer_opts()
 					,ultoa(cfg.max_batdn,tmp,10),5,K_EDIT|K_NUMBER);
 				cfg.max_batdn=atoi(tmp);
 				break;
-			case 3:
-				uifc.helpbuf=
-					"`Maximum Destination Users in User to User Transfer:`\n"
-					"\n"
-					"This is the maximum number of users allowed in the destination user list\n"
-					"of a user to user upload.\n"
-				;
-				uifc.input(WIN_MID,0,0
-					,"Maximum Destination Users in User to User Transfers"
-					,ultoa(cfg.max_userxfer,tmp,10),5,K_EDIT|K_NUMBER);
-				cfg.max_userxfer=atoi(tmp);
-				break;
-			case 4:
+			case __COUNTER__:
 	uifc.helpbuf=
 		"`Default Percentage of Credits to Credit Uploader on Upload:`\n"
 		"\n"
@@ -157,7 +137,7 @@ void xfer_opts()
 					,ultoa(cfg.cdt_up_pct,tmp,10),4,K_EDIT|K_NUMBER);
 				cfg.cdt_up_pct=atoi(tmp);
 				break;
-			case 5:
+			case __COUNTER__:
 	uifc.helpbuf=
 		"`Default Percentage of Credits to Credit Uploader on Download:`\n"
 		"\n"
@@ -169,27 +149,8 @@ void xfer_opts()
 					,ultoa(cfg.cdt_dn_pct,tmp,10),4,K_EDIT|K_NUMBER);
 				cfg.cdt_dn_pct=atoi(tmp);
 				break;
-			case 6:
-				i=0;
-				uifc.helpbuf=
-					"`Long Filenames in File Listings:`\n"
-					"\n"
-					"If you want long filenames to be displayed in the BBS file listings, set\n"
-					"this option to `Yes`. Note: This feature requires Windows 98, Windows 2000\n"
-					"or later.\n"
-				;
-				i=uifc.list(WIN_MID|WIN_SAV,0,0,0,&i,0
-					,"Long Filenames in Listings (Win98/Win2K)",uifcYesNoOpts);
-				if(!i && cfg.file_misc&FM_NO_LFN) {
-					cfg.file_misc&=~FM_NO_LFN;
-					uifc.changes=1;
-				} else if(i==1 && !(cfg.file_misc&FM_NO_LFN)) {
-					cfg.file_misc|=FM_NO_LFN;
-					uifc.changes=1;
-				}
-				break;
 
-			case 7:
+			case __COUNTER__:
 				uifc.helpbuf=
 					"`Leech Protocol Detection Percentage:`\n"
 					"\n"
@@ -219,7 +180,7 @@ void xfer_opts()
 					,ultoa(cfg.leech_sec,tmp,10),3,K_EDIT|K_NUMBER);
 				cfg.leech_sec=atoi(tmp);
 				break;
-			case 8: 	/* Viewable file types */
+			case __COUNTER__: 	/* Viewable file types */
 				while(1) {
 					for(i=0;i<cfg.total_fviews && i<MAX_OPTS;i++)
 						sprintf(opt[i],"%-3.3s  %-40s",cfg.fview[i]->ext,cfg.fview[i]->cmd);
@@ -337,7 +298,7 @@ void xfer_opts()
 					} 
 				}
 				break;
-			case 9:    /* Testable file types */
+			case __COUNTER__:    /* Testable file types */
 				while(1) {
 					for(i=0;i<cfg.total_ftests && i<MAX_OPTS;i++)
 						sprintf(opt[i],"%-3.3s  %-40s",cfg.ftest[i]->ext,cfg.ftest[i]->cmd);
@@ -472,7 +433,7 @@ void xfer_opts()
 					} 
 				}
 				break;
-			case 10:    /* Download Events */
+			case __COUNTER__:    /* Download Events */
 				while(1) {
 					for(i=0;i<cfg.total_dlevents && i<MAX_OPTS;i++)
 						sprintf(opt[i],"%-3.3s  %-40s",cfg.dlevent[i]->ext,cfg.dlevent[i]->cmd);
@@ -606,7 +567,7 @@ void xfer_opts()
 					} 
 				}
 				break;
-			case 11:	 /* Extractable file types */
+			case __COUNTER__:	 /* Extractable file types */
 				while(1) {
 					for(i=0;i<cfg.total_fextrs && i<MAX_OPTS;i++)
 						sprintf(opt[i],"%-3.3s  %-40s"
@@ -727,7 +688,7 @@ void xfer_opts()
 					} 
 				}
 				break;
-			case 12:	 /* Compressable file types */
+			case __COUNTER__:	 /* Compressible file types */
 				while(1) {
 					for(i=0;i<cfg.total_fcomps && i<MAX_OPTS;i++)
 						sprintf(opt[i],"%-3.3s  %-40s",cfg.fcomp[i]->ext,cfg.fcomp[i]->cmd);
@@ -740,13 +701,13 @@ void xfer_opts()
 					if(savfcomp.cmd[0])
 						i|=WIN_PASTE;
 					uifc.helpbuf=
-						"`Compressable File Types:`\n"
+						"`Compressible File Types:`\n"
 						"\n"
 						"This is a list of compression methods available for different file types.\n"
 						"These will be used for items such as creating QWK packets, temporary\n"
 						"files from the transfer section, and more.\n"
 					;
-					i=uifc.list(i,0,0,50,&fcomp_dflt,&fcomp_bar,"Compressable File Types",opt);
+					i=uifc.list(i,0,0,50,&fcomp_dflt,&fcomp_bar,"Compressible File Types",opt);
 					if(i==-1)
 						break;
 					int msk = i & MSK_ON;
@@ -821,13 +782,13 @@ void xfer_opts()
 							,cfg.fcomp[i]->arstr);
 						opt[j][0]=0;
 						switch(uifc.list(WIN_RHT|WIN_BOT|WIN_SAV|WIN_ACT,0,0,0,&fcomp_opt,0
-							,"Compressable File Type",opt)) {
+							,"Compressible File Type",opt)) {
 							case -1:
 								done=1;
 								break;
 							case 0:
 								uifc.input(WIN_MID|WIN_SAV,0,0
-									,"Compressable File Type Extension"
+									,"Compressible File Type Extension"
 									,cfg.fcomp[i]->ext,sizeof(cfg.fcomp[i]->ext)-1,K_EDIT);
 								break;
 							case 1:
@@ -837,7 +798,7 @@ void xfer_opts()
 									,cfg.fcomp[i]->cmd,sizeof(cfg.fcomp[i]->cmd)-1,K_EDIT);
 								break;
 							case 2:
-								sprintf(str,"Compressable File Type %s"
+								sprintf(str,"Compressible File Type %s"
 									,cfg.fcomp[i]->ext);
 								getar(str,cfg.fcomp[i]->arstr);
 								break; 
@@ -845,7 +806,7 @@ void xfer_opts()
 					} 
 				}
 				break;
-			case 13:	/* Transfer protocols */
+			case __COUNTER__:	/* Transfer protocols */
 				while(1) {
 					for(i=0;i<cfg.total_prots && i<MAX_OPTS;i++)
 						sprintf(opt[i],"%c  %s"
@@ -865,8 +826,8 @@ void xfer_opts()
 						"files either to or from a remote user. For each protocol, you can\n"
 						"specify the mnemonic (hot-key) to use to specify that protocol, the\n"
 						"command line to use for uploads, downloads, batch uploads, batch\n"
-						"downloads, bi-directional file transfers, support of DSZLOG, and (for\n"
-						"*nix only) if it uses socket I/O or the more common stdio.\n"
+						"downloads, support of DSZLOG, and (for *nix only) if it uses socket\n"
+						"I/O or the more common stdio.\n"
 						"\n"
 						"If the protocol doesn't support a certain method of transfer, or you\n"
 						"don't wish it to be available for a certain method of transfer, leave\n"
@@ -951,8 +912,6 @@ void xfer_opts()
 							,cfg.prot[i]->batulcmd);
 						sprintf(opt[j++],"%-30.30s%-40s","Batch Download Command Line"
 							,cfg.prot[i]->batdlcmd);
-						sprintf(opt[j++],"%-30.30s%-40s","Bi-dir Command Line"
-							,cfg.prot[i]->bicmd);
 						sprintf(opt[j++],"%-30.30s%s",   "Native Executable/Script"
 							,cfg.prot[i]->misc&PROT_NATIVE ? "Yes" : "No");
 						sprintf(opt[j++],"%-30.30s%s",	 "Supports DSZLOG"
@@ -1008,12 +967,6 @@ void xfer_opts()
 									,cfg.prot[i]->batdlcmd,sizeof(cfg.prot[i]->batdlcmd)-1,K_EDIT);
 								break;
 							case 7:
-								uifc.helpbuf = SCFG_CMDLINE_PREFIX_HELP SCFG_CMDLINE_SPEC_HELP;
-								uifc.input(WIN_MID|WIN_SAV,0,0
-									,"Command"
-									,cfg.prot[i]->bicmd,sizeof(cfg.prot[i]->bicmd)-1,K_EDIT);
-								break;
-							case 8:
 								l=cfg.prot[i]->misc&PROT_NATIVE ? 0:1;
 								l=uifc.list(WIN_MID|WIN_SAV,0,0,0,&l,0
 									,"Native Executable/Script",uifcYesNoOpts);
@@ -1023,7 +976,7 @@ void xfer_opts()
 									uifc.changes=1; 
 								}
 								break; 
-							case 9:
+							case 8:
 								l=cfg.prot[i]->misc&PROT_DSZLOG ? 0:1;
 								l=uifc.list(WIN_MID|WIN_SAV,0,0,0,&l,0
 									,"Uses DSZLOG",uifcYesNoOpts);
@@ -1033,7 +986,7 @@ void xfer_opts()
 									uifc.changes=1; 
 								}
 								break; 
-							case 10:
+							case 9:
 								l=cfg.prot[i]->misc&PROT_SOCKET ? 0:1l;
 								l=uifc.list(WIN_MID|WIN_SAV,0,0,0,&l,0
 									,"Uses Socket I/O",uifcYesNoOpts);
@@ -1047,91 +1000,6 @@ void xfer_opts()
 					} 
 				}
 				break;
-			case 14:	/* Alternate file paths */
-				while(1) {
-					for(i=0;i<cfg.altpaths;i++)
-						sprintf(opt[i],"%3d: %-40s",i+1,cfg.altpath[i]);
-					opt[i][0]=0;
-					i=WIN_ACT|WIN_SAV;	/* save cause size can change */
-					if((int)cfg.altpaths<MAX_OPTS)
-						i|=WIN_INS|WIN_XTR;
-					if(cfg.altpaths)
-						i|=WIN_DEL|WIN_COPY|WIN_CUT;
-					if(savaltpath[0])
-						i|=WIN_PASTE;
-					uifc.helpbuf=
-						"`Alternate File Paths:`\n"
-						"\n"
-						"This option allows the sysop to add and configure alternate file paths\n"
-						"for files stored on drives and directories other than the configured\n"
-						"`File Transfer Path` of a file directory. This option is useful for sysops\n"
-						"that have file directories where they wish to have files listed from\n"
-						"multiple locations (CD-ROMs or hard disks).\n"
-					;
-					i=uifc.list(i,0,0,50,&altpath_dflt,&altpath_bar,"Alternate File Paths",opt);
-					if(i==-1)
-						break;
-					int msk = i & MSK_ON;
-					i &= MSK_OFF;
-					if(msk == MSK_DEL || msk == MSK_CUT) {
-						if(msk == MSK_CUT)
-							SAFECOPY(savaltpath, cfg.altpath[i]);
-						free(cfg.altpath[i]);
-						cfg.altpaths--;
-						while(i<cfg.altpaths) {
-							cfg.altpath[i]=cfg.altpath[i+1];
-							i++; 
-						}
-						uifc.changes=1;
-						continue; 
-					}
-					if(msk == MSK_INS) {
-						if((cfg.altpath=(char **)realloc(cfg.altpath
-							,sizeof(char *)*(cfg.altpaths+1)))==NULL) {
-							errormsg(WHERE,ERR_ALLOC,nulstr,cfg.altpaths+1);
-							cfg.altpaths=0;
-							bail(1);
-							continue; 
-						}
-						if(!cfg.altpaths) {
-							if((cfg.altpath[0]=(char *)malloc(LEN_DIR+1))==NULL) {
-								errormsg(WHERE,ERR_ALLOC,nulstr,LEN_DIR+1);
-								continue; 
-							}
-							memset(cfg.altpath[0],0,LEN_DIR+1); 
-						}
-						else {
-							for(j=cfg.altpaths;j>i;j--)
-								cfg.altpath[j]=cfg.altpath[j-1];
-							if((cfg.altpath[i]=(char *)malloc(LEN_DIR+1))==NULL) {
-								errormsg(WHERE,ERR_ALLOC,nulstr,LEN_DIR+1);
-								continue; 
-							}
-							if(i>=cfg.altpaths)
-								j=i-1;
-							else
-								j=i+1;
-							memcpy(cfg.altpath[i],cfg.altpath[j],LEN_DIR+1); 
-						}
-						cfg.altpaths++;
-						uifc.changes=1;
-						continue; 
-					}
-					if(msk == MSK_COPY) {
-						SAFECOPY(savaltpath,cfg.altpath[i]);
-						continue; 
-					}
-					if(msk == MSK_PASTE) {
-						memcpy(cfg.altpath[i],savaltpath,LEN_DIR+1);
-						uifc.changes=1;
-						continue; 
-					}
-					if (msk != 0)
-						continue;
-					sprintf(str,"Path %d",i+1);
-					uifc.input(WIN_MID|WIN_SAV,0,0,str,cfg.altpath[i],LEN_DIR,K_EDIT); 
-				}
-				break; 
 		} 
 	}
 }
diff --git a/src/sbbs3/scfg/scfgxfr2.c b/src/sbbs3/scfg/scfgxfr2.c
index 380df8906906bf6b9cb0f0e7254ad41d49f891a7..40bb5d57145d15f76e67490f806ee16f95fd87a7 100644
--- a/src/sbbs3/scfg/scfgxfr2.c
+++ b/src/sbbs3/scfg/scfgxfr2.c
@@ -23,6 +23,16 @@
 #define DEFAULT_DIR_OPTIONS (DIR_FCHK|DIR_MULT|DIR_DUPES|DIR_CDTUL|DIR_CDTDL|DIR_DIZ)
 #define CUT_LIBNUM	USHRT_MAX
 
+char* file_sort_desc[] = {
+	"Name Ascending (case-insensitive)",
+	"Name Descending (case-insensitive)",
+	"Date Ascending",
+	"Date Descending",
+	"Name Ascending (case-sensitive)",
+	"Name Descending (case-sensitive)",
+	NULL
+};
+
 static bool new_dir(unsigned new_dirnum, unsigned libnum)
 {
 	dir_t* new_directory = malloc(sizeof(dir_t));
@@ -32,7 +42,6 @@ static bool new_dir(unsigned new_dirnum, unsigned libnum)
 	}
 	memset(new_directory, 0, sizeof(*new_directory));
 	new_directory->lib = libnum;
-	new_directory->maxfiles = MAX_FILES;
 	new_directory->misc = DEFAULT_DIR_OPTIONS;
 	new_directory->up_pct = cfg.cdt_up_pct;
 	new_directory->dn_pct = cfg.cdt_dn_pct;
@@ -603,8 +612,9 @@ void xfer_cfg()
 							continue;
 						ported++;
 						if(k==1) {
-							fprintf(stream,"Area %-8s  0     !      %s\n"
-								,cfg.dir[j]->code_suffix,cfg.dir[j]->lname);
+							fprintf(stream,"Area %-*s  0     !      %s\n"
+								,FIDO_AREATAG_LEN
+								,dir_area_tag(&cfg, cfg.dir[j], str, sizeof(str)) ,cfg.dir[j]->lname);
 							continue;
 						}
 						fprintf(stream,"%s\n%s\n%s\n%s\n%s\n%s\n"
@@ -711,7 +721,6 @@ void xfer_cfg()
 							continue;
 						memset(&tmpdir,0,sizeof(dir_t));
 						tmpdir.misc=DEFAULT_DIR_OPTIONS;
-						tmpdir.maxfiles=MAX_FILES;
 						tmpdir.up_pct=cfg.cdt_up_pct;
 						tmpdir.dn_pct=cfg.cdt_dn_pct; 
 
@@ -761,8 +770,9 @@ void xfer_cfg()
 							while(*p && *p<=' ') p++;	/* Skip space */
 							while(*p>' ') p++;			/* Skip flags */
 							while(*p && *p<=' ') p++;	/* Skip space */
-							SAFECOPY(tmpdir.sname,tmp_code); 
-							SAFECOPY(tmpdir.lname,p); 
+							SAFECOPY(tmpdir.sname,tmp_code);
+							SAFECOPY(tmpdir.area_tag,tmp_code);
+							SAFECOPY(tmpdir.lname,p);
 							ported++;
 						}
 						else {
@@ -898,7 +908,6 @@ void xfer_cfg()
 							SAFECOPY(cfg.dir[j]->sname,tmpdir.sname);
 							SAFECOPY(cfg.dir[j]->lname,tmpdir.lname);
 							if(j==cfg.total_dirs) {
-								cfg.dir[j]->maxfiles=MAX_FILES;
 								cfg.dir[j]->up_pct=cfg.cdt_up_pct;
 								cfg.dir[j]->dn_pct=cfg.cdt_dn_pct; 
 							}
@@ -1132,6 +1141,7 @@ void dir_cfg(uint libnum)
 			sprintf(opt[n++],"%-27.27s%s","Short Name",cfg.dir[i]->sname);
 			sprintf(opt[n++],"%-27.27s%s%s","Internal Code"
 				,cfg.lib[cfg.dir[i]->lib]->code_prefix, cfg.dir[i]->code_suffix);
+			sprintf(opt[n++],"%-27.27s%s","Area Tag",cfg.dir[i]->area_tag);
 			sprintf(opt[n++],"%-27.27s%s","Access Requirements"
 				,cfg.dir[i]->arstr);
 			sprintf(opt[n++],"%-27.27s%s","Upload Requirements"
@@ -1162,8 +1172,12 @@ void dir_cfg(uint libnum)
 				SAFEPRINTF(str, "[%s]", path);
 			sprintf(opt[n++],"%-27.27s%s","Transfer File Path"
 				,str);
-			sprintf(opt[n++],"%-27.27s%u","Maximum Number of Files"
-				,cfg.dir[i]->maxfiles);
+			if(cfg.dir[i]->maxfiles)
+				sprintf(str, "%u", cfg.dir[i]->maxfiles);
+			else
+				SAFECOPY(str, "Unlimited");
+			sprintf(opt[n++],"%-27.27s%s","Maximum Number of Files"
+				,str);
 			if(cfg.dir[i]->maxage)
 				sprintf(str,"Enabled (%u days old)",cfg.dir[i]->maxage);
 			else
@@ -1216,48 +1230,49 @@ void dir_cfg(uint libnum)
 					}
 					break;
 				case 3:
+					uifc.helpbuf="FidoNet Area Tag!";
+					SAFECOPY(str, cfg.dir[i]->area_tag);
+					if(uifc.input(WIN_L2R|WIN_SAV,0,17,"FidoNet File Echo Area Tag"
+						,str, FIDO_AREATAG_LEN, K_EDIT | K_UPPER) > 0)
+						SAFECOPY(cfg.dir[i]->area_tag, str);
+					break;
+				case 4:
 					sprintf(str,"%s Access",cfg.dir[i]->sname);
 					getar(str,cfg.dir[i]->arstr);
 					break;
-				case 4:
+				case 5:
 					sprintf(str,"%s Upload",cfg.dir[i]->sname);
 					getar(str,cfg.dir[i]->ul_arstr);
 					break;
-				case 5:
+				case 6:
 					sprintf(str,"%s Download",cfg.dir[i]->sname);
 					getar(str,cfg.dir[i]->dl_arstr);
 					break;
-				case 6:
+				case 7:
 					sprintf(str,"%s Operator",cfg.dir[i]->sname);
 					getar(str,cfg.dir[i]->op_arstr);
 					break;
-				case 7:
+				case 8:
 					sprintf(str,"%s Exemption",cfg.dir[i]->sname);
 					getar(str,cfg.dir[i]->ex_arstr);
 					break;
-				case 8:
+				case 9:
 					uifc.helpbuf = dir_transfer_path_help;
 					uifc.input(WIN_L2R|WIN_SAV,0,17,"Transfer File Path"
 						,cfg.dir[i]->path,sizeof(cfg.dir[i]->path)-1,K_EDIT);
 					break;
-				case 9:
+				case 10:
 					uifc.helpbuf=
 						"`Maximum Number of Files:`\n"
 						"\n"
 						"This value is the maximum number of files allowed in this directory.\n"
 					;
 					sprintf(str,"%u",cfg.dir[i]->maxfiles);
-					uifc.input(WIN_L2R|WIN_SAV,0,17,"Maximum Number of Files"
+					uifc.input(WIN_L2R|WIN_SAV,0,17,"Maximum Number of Files (0=Unlimited)"
 						,str,5,K_EDIT|K_NUMBER);
-					n=atoi(str);
-					if(n>MAX_FILES) {
-						sprintf(str,"Maximum Files is %u",MAX_FILES);
-						uifc.msg(str);
-					}
-					else
-						cfg.dir[i]->maxfiles=n;
+					cfg.dir[i]->maxfiles=atoi(str);
 					break;
-				case 10:
+				case 11:
 					sprintf(str,"%u",cfg.dir[i]->maxage);
 					uifc.helpbuf=
 						"`Maximum Age of Files:`\n"
@@ -1273,7 +1288,7 @@ void dir_cfg(uint libnum)
 						,str,5,K_EDIT|K_NUMBER);
 					cfg.dir[i]->maxage=atoi(str);
 					break;
-				case 11:
+				case 12:
 	uifc.helpbuf=
 		"`Percentage of Credits to Credit Uploader on Upload:`\n"
 		"\n"
@@ -1289,7 +1304,7 @@ void dir_cfg(uint libnum)
 						,ultoa(cfg.dir[i]->up_pct,tmp,10),4,K_EDIT|K_NUMBER);
 					cfg.dir[i]->up_pct=atoi(tmp);
 					break;
-				case 12:
+				case 13:
 	uifc.helpbuf=
 		"`Percentage of Credits to Credit Uploader on Download:`\n"
 		"\n"
@@ -1306,7 +1321,7 @@ void dir_cfg(uint libnum)
 						,ultoa(cfg.dir[i]->dn_pct,tmp,10),4,K_EDIT|K_NUMBER);
 					cfg.dir[i]->dn_pct=atoi(tmp);
 					break;
-				case 13:
+				case 14:
 					while(1) {
 						n=0;
 						sprintf(opt[n++],"%-30.30s%s","Check for File Existence"
@@ -1355,10 +1370,12 @@ void dir_cfg(uint libnum)
 							,cfg.dir[i]->misc&DIR_MOVENEW ? "Yes":"No");
 						sprintf(opt[n++],"%-30.30s%s","Include Transfers In Stats"
 							,cfg.dir[i]->misc&DIR_NOSTAT ? "No":"Yes");
-						sprintf(opt[n++],"%-30.30s%s","Access Files not in Database"
-							,cfg.dir[i]->misc&DIR_FILES ? "Yes":"No");
+						sprintf(opt[n++],"%-30.30s%s","Calculate/Store Hash of Files"
+							,cfg.dir[i]->misc&DIR_NOHASH ? "No":"Yes");
 						sprintf(opt[n++],"%-30.30s%s","Template for New Directories"
 							,cfg.dir[i]->misc&DIR_TEMPLATE ? "Yes" : "No");
+						sprintf(opt[n++],"%-30.30s%s","Allow File Tagging"
+							,cfg.dir[i]->misc&DIR_FILETAGS ? "Yes" : "No");
 						opt[n][0]=0;
 						uifc.helpbuf=
 							"`Directory Toggle Options:`\n"
@@ -1799,22 +1816,25 @@ void dir_cfg(uint libnum)
 								}
 								break;
 							case 20:
-								n=cfg.dir[i]->misc&DIR_FILES ? 0:1;
+								n=cfg.dir[i]->misc&DIR_NOHASH ? 1:0;
 								uifc.helpbuf=
-									"`Allow Access to Files Not in Database:`\n"
+									"`Calculate/Store Hashes of Files:`\n"
 									"\n"
-									"If this option is set to ~Yes~, then all files in this directory's\n"
-									"`Transfer File Path` will be visible/downloadable by users with access to\n"
-									"this directory.\n"
+									"Set to ~Yes~ to calculate and store the hashes of file contents when\n"
+									"adding files to this file base.\n"
+									"\n"
+									"The hashes (CRC-16, CRC-32, MD5, and SHA-1) are useful for detecting\n"
+									"duplicate files (i.e. and rejecting them) as well as allowing the\n"
+									"confirmation of data integrity for the downloaders of files."
 								;
 								n=uifc.list(WIN_MID|WIN_SAV,0,0,0,&n,0
-									,"Allow Access to Files Not in Database"
+									,"Calculate/Store Hashes of Files"
 									,uifcYesNoOpts);
-								if(n==0 && !(cfg.dir[i]->misc&DIR_FILES)) {
-									cfg.dir[i]->misc|=DIR_FILES;
+								if(n==0 && cfg.dir[i]->misc&DIR_NOHASH) {
+									cfg.dir[i]->misc &= ~DIR_NOHASH;
 									uifc.changes=1; 
-								} else if(n==1 && cfg.dir[i]->misc&DIR_FILES){
-									cfg.dir[i]->misc&=~DIR_FILES;
+								} else if(n==1 && !(cfg.dir[i]->misc&DIR_NOHASH)){
+									cfg.dir[i]->misc |= DIR_NOHASH;
 									uifc.changes=1; 
 								}
 								break;
@@ -1844,10 +1864,30 @@ void dir_cfg(uint libnum)
 									cfg.dir[i]->misc&=~DIR_TEMPLATE; 
 								}
 								break;
+							case 22:
+								n=(cfg.dir[i]->misc&DIR_FILETAGS) ? 0:1;
+								uifc.helpbuf=
+									"`Allow Addition of Tags to Files:`\n"
+									"\n"
+								;
+								n=uifc.list(WIN_SAV|WIN_MID,0,0,0,&n,0
+									,"Allow Addition of Tags to Files",uifcYesNoOpts);
+								if(n==-1)
+									break;
+								if(!n && !(cfg.dir[i]->misc & DIR_FILETAGS)) {
+									uifc.changes = TRUE;
+									cfg.dir[i]->misc |= DIR_FILETAGS;
+									break; 
+								}
+								if(n==1 && (cfg.dir[i]->misc&DIR_FILETAGS)) {
+									uifc.changes = TRUE;
+									cfg.dir[i]->misc &= ~DIR_FILETAGS; 
+								}
+								break;
 						} 
 					}
 					break;
-			case 14:
+			case 15:
 				while(1) {
 					n=0;
 					sprintf(opt[n++],"%-27.27s%s","Extensions Allowed"
@@ -1861,10 +1901,7 @@ void dir_cfg(uint libnum)
 					sprintf(opt[n++],"%-27.27s%s","Upload Semaphore File"
 						,cfg.dir[i]->upload_sem);
 					sprintf(opt[n++],"%-27.27s%s","Sort Value and Direction"
-						, cfg.dir[i]->sort==SORT_NAME_A ? "Name Ascending"
-						: cfg.dir[i]->sort==SORT_NAME_D ? "Name Descending"
-						: cfg.dir[i]->sort==SORT_DATE_A ? "Date Ascending"
-						: "Date Descending");
+						,file_sort_desc[cfg.dir[i]->sort]);
 					sprintf(opt[n++],"%-27.27sNow %u / Was %u","Directory Index", i, cfg.dir[i]->dirnum);
 					opt[n][0]=0;
 					uifc.helpbuf=
@@ -1872,7 +1909,7 @@ void dir_cfg(uint libnum)
 						"\n"
 						"This is the advanced options menu for the selected file directory.\n"
 					;
-						n=uifc.list(WIN_ACT|WIN_SAV|WIN_RHT|WIN_BOT,3,4,60,&adv_dflt,0
+						n=uifc.list(WIN_ACT|WIN_SAV|WIN_RHT|WIN_BOT,3,4,65,&adv_dflt,0
 							,"Advanced Options",opt);
 						if(n==-1)
 							break;
@@ -1914,38 +1951,22 @@ void dir_cfg(uint libnum)
 									,cfg.dir[i]->upload_sem,sizeof(cfg.dir[i]->upload_sem)-1,K_EDIT);
 								break;
 							case 3:
-								n=0;
-								strcpy(opt[0],"Name Ascending");
-								strcpy(opt[1],"Name Descending");
-								strcpy(opt[2],"Date Ascending");
-								strcpy(opt[3],"Date Descending");
-								opt[4][0]=0;
+								n = cfg.dir[i]->sort;
 								uifc.helpbuf=
 									"`Sort Value and Direction:`\n"
 									"\n"
-									"This option allows you to determine the sort value and direction. Files\n"
-									"that are uploaded are automatically sorted by filename or upload date,\n"
-									"ascending or descending. If you change the sort value or direction after\n"
-									"a directory already has files in it, use the sysop transfer menu `;RESORT`\n"
-									"command to resort the directory with the new sort parameters.\n"
+									"This option allows you to determine the sort value and direction for\n"
+									"the display of file listings.\n"
+									"\n"
+									"The dates available for sorting are the file import/upload date/time.\n"
+									"\n"
+									"The natural (and thus fastest) sort order is `Date Ascending`."
 								;
 								n=uifc.list(WIN_MID|WIN_SAV,0,0,0,&n,0
-									,"Sort Value and Direction",opt);
-								if(n==0 && cfg.dir[i]->sort!=SORT_NAME_A) {
-									cfg.dir[i]->sort=SORT_NAME_A;
-									uifc.changes=1;
-								}
-								else if(n==1 && cfg.dir[i]->sort!=SORT_NAME_D) {
-									cfg.dir[i]->sort=SORT_NAME_D;
-									uifc.changes=1;
-								}
-								else if(n==2 && cfg.dir[i]->sort!=SORT_DATE_A) {
-									cfg.dir[i]->sort=SORT_DATE_A;
-									uifc.changes=1;
-								}
-								else if(n==3 && cfg.dir[i]->sort!=SORT_DATE_D) {
-									cfg.dir[i]->sort=SORT_DATE_D;
-									uifc.changes=1;
+									,"Sort Value and Direction", file_sort_desc);
+								if(n >= 0 && cfg.dir[i]->sort != n) {
+									cfg.dir[i]->sort = n;
+									uifc.changes = TRUE;
 								}
 								break; 
 							case 4:
diff --git a/src/sbbs3/scfgdefs.h b/src/sbbs3/scfgdefs.h
index a23caa4eae43c646ed607aca31e6cbbd7a594835..6ebf654d046f39b5b1e4f46da877e4642d5587bc 100644
--- a/src/sbbs3/scfgdefs.h
+++ b/src/sbbs3/scfgdefs.h
@@ -72,6 +72,7 @@ typedef struct {							/* Message group info */
 typedef struct {							/* Transfer Directory Info */
 	char		code[LEN_EXTCODE+1];		/* Internal code (with optional lib prefix) */
 	char		code_suffix[LEN_CODE+1];	/* Eight character code suffix */
+	char		area_tag[FIDO_AREATAG_LEN+1];
 	char		lname[LEN_SLNAME+1],		/* Long name - used for listing */
 				sname[LEN_SSNAME+1],		/* Short name - used for prompts */
 				arstr[LEN_ARSTR+1],			/* Access Requirements */
@@ -311,6 +312,7 @@ typedef struct {							/* QWK Network Hub */
 				call[LEN_CMD+1],			/* Call-out command line to execute */
 				pack[LEN_CMD+1],			/* Packing command line */
 				unpack[LEN_CMD+1];			/* Unpacking command line */
+	char		fmt[4]; 					/* Archive format */
 	uint16_t	time,						/* Time to call-out */
 				node,						/* Node to do the call-out */
 				freq,						/* Frequency of call-outs */
diff --git a/src/sbbs3/scfglib.h b/src/sbbs3/scfglib.h
index 3353552d37264571cf8a84bd8ee7c139ed997b63..0848464da050c466900263279f736472d8026b9d 100644
--- a/src/sbbs3/scfglib.h
+++ b/src/sbbs3/scfglib.h
@@ -61,6 +61,11 @@ long	aftol(char *str);              /* Converts flag string to long */
 char*	ltoaf(long l, char *str);     /* Converts long to flag string */
 uint	attrstr(char *str);		/* Convert ATTR string into attribute int */
 
+int		getdirnum(scfg_t*, const char* code);
+int		getlibnum(scfg_t*, const char* code);
+int		getsubnum(scfg_t*, const char* code);
+int		getgrpnum(scfg_t*, const char* code);
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/src/sbbs3/scfglib1.c b/src/sbbs3/scfglib1.c
index 966dbf90871b044b62a4f6cf17b6b8853d760bff..f274307026e731eb0449316a818463476895243b 100644
--- a/src/sbbs3/scfglib1.c
+++ b/src/sbbs3/scfglib1.c
@@ -625,7 +625,8 @@ BOOL read_msgs_cfg(scfg_t* cfg, char* error, size_t maxerrlen)
 			}
 		}
 		get_int(cfg->qhub[i]->misc, instream);
-		for(j=0;j<30;j++)
+		get_str(cfg->qhub[i]->fmt,instream);
+		for(j=0;j<28;j++)
 			get_int(n,instream);
 	}
 
@@ -812,3 +813,55 @@ void make_data_dirs(scfg_t* cfg)
 	}
 #endif
 }
+
+int getdirnum(scfg_t* cfg, const char* code)
+{
+	size_t i;
+
+	if(code == NULL || *code == '\0')
+		return -1;
+
+	for(i=0;i<cfg->total_dirs;i++)
+		if(stricmp(cfg->dir[i]->code,code)==0)
+			return(i);
+	return(-1);
+}
+
+int getlibnum(scfg_t* cfg, const char* code)
+{
+	size_t i;
+
+	if(code == NULL || *code == '\0')
+		return -1;
+
+	for(i=0;i<cfg->total_dirs;i++)
+		if(stricmp(cfg->dir[i]->code,code)==0)
+			return(cfg->dir[i]->lib);
+	return(-1);
+}
+
+int getsubnum(scfg_t* cfg, const char* code)
+{
+	size_t i;
+
+	if(code == NULL || *code == '\0')
+		return -1;
+
+	for(i=0;i<cfg->total_subs;i++)
+		if(stricmp(cfg->sub[i]->code,code)==0)
+			return(i);
+	return(-1);
+}
+
+int getgrpnum(scfg_t* cfg, const char* code)
+{
+	size_t i;
+
+	if(code == NULL || *code == '\0')
+		return -1;
+
+	for(i=0;i<cfg->total_subs;i++)
+		if(stricmp(cfg->sub[i]->code,code)==0)
+			return(cfg->sub[i]->grp);
+	return(-1);
+}
diff --git a/src/sbbs3/scfglib2.c b/src/sbbs3/scfglib2.c
index a6f9fc6c4a703d70746c07a2c1988ce0736ad294..ccba11fcf6e090674343a68e948623c690d68069 100644
--- a/src/sbbs3/scfglib2.c
+++ b/src/sbbs3/scfglib2.c
@@ -361,8 +361,6 @@ BOOL read_file_cfg(scfg_t* cfg, char* error, size_t maxerrlen)
 		get_str(cfg->dir[i]->upload_sem,instream);
 
 		get_int(cfg->dir[i]->maxfiles,instream);
-		if(cfg->dir[i]->maxfiles>MAX_FILES)
-			cfg->dir[i]->maxfiles=MAX_FILES;
 		get_str(cfg->dir[i]->exts,instream);
 		get_int(cfg->dir[i]->misc,instream);
 		get_int(cfg->dir[i]->seqdev,instream);
@@ -373,8 +371,9 @@ BOOL read_file_cfg(scfg_t* cfg, char* error, size_t maxerrlen)
 		get_int(cfg->dir[i]->maxage,instream);
 		get_int(cfg->dir[i]->up_pct,instream);
 		get_int(cfg->dir[i]->dn_pct,instream);
+		get_str(cfg->dir[i]->area_tag,instream);
 		get_int(c,instream);
-		for(j=0;j<24;j++)
+		for(j=0;j<6;j++)
 			get_int(n,instream); 
 	}
 
diff --git a/src/sbbs3/scfgsave.c b/src/sbbs3/scfgsave.c
index cbf258496d70fc21f1694ff40631713dc5d882ac..5be3129b0d4d4560f28446d7dae8198f283206fb 100644
--- a/src/sbbs3/scfgsave.c
+++ b/src/sbbs3/scfgsave.c
@@ -540,8 +540,9 @@ BOOL DLLCALL write_msgs_cfg(scfg_t* cfg, int backup_level)
 			put_int(cfg->qhub[i]->mode[j],stream); 
 		}
 		put_int(cfg->qhub[i]->misc, stream);
+		put_str(cfg->qhub[i]->fmt,stream);
 		n=0;
-		for(j=0;j<30;j++)
+		for(j=0;j<28;j++)
 			put_int(n,stream); 
 	}
 	n=0;
@@ -835,13 +836,11 @@ BOOL DLLCALL write_file_cfg(scfg_t* cfg, int backup_level)
 				put_int(cfg->dir[i]->maxage, stream);
 				put_int(cfg->dir[i]->up_pct, stream);
 				put_int(cfg->dir[i]->dn_pct, stream);
+				put_str(cfg->dir[i]->area_tag, stream);
 				c = 0;
 				put_int(c, stream);
-				n = 0;
-				for (k = 0; k < 8; k++)
-					put_int(n, stream);
 				n = 0xffff;
-				for (k = 0; k < 16; k++)
+				for (k = 0; k < 6; k++)
 					put_int(n, stream);
 			}
 		}
@@ -1092,54 +1091,3 @@ void DLLCALL refresh_cfg(scfg_t* cfg)
 
 	SAFEPRINTF(str,"%srecycle",cfg->ctrl_dir);		ftouch(str);
 }
-
-int DLLCALL smb_storage_mode(scfg_t* cfg, smb_t* smb)
-{
-	if(smb == NULL || smb->subnum == INVALID_SUB || (smb->status.attr&SMB_EMAIL))
-		return (cfg->sys_misc&SM_FASTMAIL) ? SMB_FASTALLOC : SMB_SELFPACK;
-	if(smb->subnum >= cfg->total_subs)
-		return (smb->status.attr&SMB_HYPERALLOC) ? SMB_HYPERALLOC : SMB_FASTALLOC;
-	if(cfg->sub[smb->subnum]->misc&SUB_HYPER) {
-		smb->status.attr |= SMB_HYPERALLOC;
-		return SMB_HYPERALLOC;
-	}
-	if(cfg->sub[smb->subnum]->misc&SUB_FAST)
-		return SMB_FASTALLOC;
-	return SMB_SELFPACK;
-}
-
-/* Open Synchronet Message Base and create, if necessary (e.g. first time opened) */
-/* If return value is not SMB_SUCCESS, sub-board is not left open */
-int DLLCALL smb_open_sub(scfg_t* cfg, smb_t* smb, unsigned int subnum)
-{
-	int retval;
-	smbstatus_t smb_status = {0};
-
-	if(subnum != INVALID_SUB && subnum >= cfg->total_subs)
-		return SMB_FAILURE;
-	memset(smb, 0, sizeof(smb_t));
-	if(subnum == INVALID_SUB) {
-		SAFEPRINTF(smb->file, "%smail", cfg->data_dir);
-		smb_status.max_crcs	= cfg->mail_maxcrcs;
-		smb_status.max_msgs	= 0;
-		smb_status.max_age	= cfg->mail_maxage;
-		smb_status.attr		= SMB_EMAIL;
-	} else {
-		SAFEPRINTF2(smb->file, "%s%s", cfg->sub[subnum]->data_dir, cfg->sub[subnum]->code);
-		smb_status.max_crcs	= cfg->sub[subnum]->maxcrcs;
-		smb_status.max_msgs	= cfg->sub[subnum]->maxmsgs;
-		smb_status.max_age	= cfg->sub[subnum]->maxage;
-		smb_status.attr		= cfg->sub[subnum]->misc&SUB_HYPER ? SMB_HYPERALLOC :0;
-	}
-	smb->retry_time = cfg->smb_retry_time;
-	if((retval = smb_open(smb)) == SMB_SUCCESS) {
-		if(smb_fgetlength(smb->shd_fp) < sizeof(smbhdr_t) + sizeof(smb->status)) {
-			smb->status = smb_status;
-			if((retval = smb_create(smb)) != SMB_SUCCESS)
-				smb_close(smb);
-		}
-		if(retval == SMB_SUCCESS)
-			smb->subnum = subnum;
-	}
-	return retval;
-}
diff --git a/src/sbbs3/scfgsave.h b/src/sbbs3/scfgsave.h
index e0363a72bbc6ddd06017ea6bbaa74e3d480ade38..15ce25b7cfce9e89bf9050a137b25c32175619a4 100644
--- a/src/sbbs3/scfgsave.h
+++ b/src/sbbs3/scfgsave.h
@@ -21,7 +21,6 @@
 #define _SCFGSAVE_H_
 
 #include "scfgdefs.h"
-#include "smbdefs.h"
 #include "dllexport.h"
 
 #ifdef __cplusplus
@@ -36,8 +35,6 @@ DLLEXPORT BOOL		write_file_cfg(scfg_t* cfg, int backup_level);
 DLLEXPORT BOOL		write_chat_cfg(scfg_t* cfg, int backup_level);
 DLLEXPORT BOOL		write_xtrn_cfg(scfg_t* cfg, int backup_level);
 DLLEXPORT void		refresh_cfg(scfg_t* cfg);
-DLLEXPORT int		smb_storage_mode(scfg_t*, smb_t*);
-DLLEXPORT int		smb_open_sub(scfg_t*, smb_t*, unsigned int subnum);
 
 #ifdef __cplusplus
 }
diff --git a/src/sbbs3/services.c b/src/sbbs3/services.c
index 11efc7f59b38a1862f4eb4158edb2d080a1d14cf..e1067a2d83d9d2a1f56138e878208baf2e5ef878 100644
--- a/src/sbbs3/services.c
+++ b/src/sbbs3/services.c
@@ -47,7 +47,8 @@
 #include "js_socket.h"
 #include "multisock.h"
 #include "ssl.h"
-#include "ver.h"
+#include "git_branch.h"
+#include "git_hash.h"
 
 /* Constants */
 
@@ -794,6 +795,10 @@ js_initcx(JSRuntime* js_runtime, SOCKET sock, service_client_t* service_client,
 		if(js_CreateMsgBaseClass(js_cx, *glob, &scfg)==NULL)
 			break;
 
+		/* FileBase Class */
+		if(js_CreateFileBaseClass(js_cx, *glob, &scfg)==NULL)
+			break;
+
 		/* File Class */
 		if(js_CreateFileClass(js_cx, *glob)==NULL)
 			break;
@@ -1693,7 +1698,7 @@ const char* DLLCALL services_ver(void)
 #else
 		,""
 #endif
-		,git_branch, git_hash
+		,GIT_BRANCH, GIT_HASH
 		,__DATE__, __TIME__, compiler
 		);
 
@@ -1851,7 +1856,7 @@ void DLLCALL services_thread(void* arg)
 
 		DESCRIBE_COMPILER(compiler);
 
-		lprintf(LOG_INFO,"Compiled %s/%s %s %s with %s", git_branch, git_hash, __DATE__, __TIME__, compiler);
+		lprintf(LOG_INFO,"Compiled %s/%s %s %s with %s", GIT_BRANCH, GIT_HASH, __DATE__, __TIME__, compiler);
 
 		protected_uint32_init(&threads_pending_start,0);
 
diff --git a/src/sbbs3/services.vcxproj b/src/sbbs3/services.vcxproj
index 57beac0b879c45d42777eafddc1313bed0a962f5..b84a464865002755a5cb4da625dedd9ded129b70 100644
--- a/src/sbbs3/services.vcxproj
+++ b/src/sbbs3/services.vcxproj
@@ -176,7 +176,6 @@
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
     </ClCompile>
-    <ClCompile Include="ver.cpp" />
   </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\xpdev\xpdev_mt.vcxproj">
diff --git a/src/sbbs3/sexyz.vcxproj b/src/sbbs3/sexyz.vcxproj
index 1951389828a87b5999c5e09e83caa398792ef4c8..68a0a5161f16fd7408748d04285b0ed98c554ec5 100644
--- a/src/sbbs3/sexyz.vcxproj
+++ b/src/sbbs3/sexyz.vcxproj
@@ -66,7 +66,7 @@
     <ClCompile>
       <Optimization>Disabled</Optimization>
       <AdditionalIncludeDirectories>%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
-      <PreprocessorDefinitions>_DEBUG;WIN32;_CONSOLE;SBBS_EXPORTS;RINGBUF_SEM;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <PreprocessorDefinitions>_DEBUG;WIN32;_CONSOLE;SBBS_EXPORTS;RINGBUF_SEM;RINGBUF_EVENT;%(PreprocessorDefinitions)</PreprocessorDefinitions>
       <BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
       <RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
       <PrecompiledHeaderOutputFile>.\msvc.win32.debug\sexyz/sexyz.pch</PrecompiledHeaderOutputFile>
@@ -110,7 +110,7 @@
       <Optimization>MaxSpeed</Optimization>
       <InlineFunctionExpansion>OnlyExplicitInline</InlineFunctionExpansion>
       <AdditionalIncludeDirectories>%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
-      <PreprocessorDefinitions>NDEBUG;WIN32;_CONSOLE;SBBS_EXPORTS;RINGBUF_SEM;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <PreprocessorDefinitions>NDEBUG;WIN32;_CONSOLE;SBBS_EXPORTS;RINGBUF_SEM;RINGBUF_EVENT;%(PreprocessorDefinitions)</PreprocessorDefinitions>
       <StringPooling>true</StringPooling>
       <RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
       <FunctionLevelLinking>true</FunctionLevelLinking>
diff --git a/src/sbbs3/slog.c b/src/sbbs3/slog.c
index 958cd656296ed08980e85cbbd5a2bd8ff639e7d8..1de08ace1e70665b706a42eb8d0ef9df8bfa439e 100644
--- a/src/sbbs3/slog.c
+++ b/src/sbbs3/slog.c
@@ -26,8 +26,8 @@ int main(int argc, char **argv)
 	int i,file,pause=0,lncntr=0;
     time_t timestamp;
     long l;
-    ulong   length,
-            logons,
+    off_t   length;
+    ulong	logons,
             timeon,
             posts,
             emails,
@@ -65,13 +65,13 @@ length=filelength(file);
 if(length<40) {
     close(file);
 	return(1); }
-if((buf=malloc(length))==0) {
+if((buf=malloc((size_t)length))==0) {
     close(file);
-	printf("error allocating %lu bytes\r\n",length);
+	printf("error allocating %lu bytes\r\n",(ulong)length);
 	return(1); }
-read(file,buf,length);
+read(file,buf,(uint)length);
 close(file);
-l=length-4;
+l=(long)(length-4);
 while(l>-1L) {
     fbacks=buf[l]|((long)buf[l+1]<<8)|((long)buf[l+2]<<16)
         |((long)buf[l+3]<<24);
diff --git a/src/sbbs3/smbactiv.c b/src/sbbs3/smbactiv.c
index ad888e6712d36fc72eab843485f3fbf2e773fb6e..f9c7f8b01d041345f64a2da8aca256b95bb5a174 100644
--- a/src/sbbs3/smbactiv.c
+++ b/src/sbbs3/smbactiv.c
@@ -36,7 +36,7 @@ ulong first_msg()
 {
 	smbmsg_t msg;
 
-	msg.offset=0;
+	msg.idx_offset=0;
 	msg.hdr.number=0;
 	if(smb_getmsgidx(&smb,&msg))			/* Get first message index */
 		return(0);
@@ -84,7 +84,8 @@ int main(int argc, char **argv)
 {
 	char str[256],*p;
 	int i,j,file;
-	ulong length,max_users=0xffffffff;
+	off_t length;
+	ulong max_users=0xffffffff;
 	uint32_t l;
 	sub_status_t *sub_status;
 	scfg_t	cfg;
diff --git a/src/sbbs3/smbutil.c b/src/sbbs3/smbutil.c
index 47afdaf4cbf7fb7d300d65fbf4b8031c7487c72b..b99c81d10f5a15babdcaca44efb1c1d3e5fce6a2 100644
--- a/src/sbbs3/smbutil.c
+++ b/src/sbbs3/smbutil.c
@@ -19,8 +19,7 @@
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
-#define SMBUTIL_VER "2.34"
-char	revision[16];
+#define SMBUTIL_VER "3.19"
 char	compiler[32];
 
 const char *wday[]={"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
@@ -53,6 +52,9 @@ const char *mon[]={"Jan","Feb","Mar","Apr","May","Jun"
 #include "str_util.h"
 #include "utf8.h"
 #include "conwrap.h"
+#include "xpdatetime.h"
+#include "git_branch.h"
+#include "git_hash.h"
 
 /* gets is dangerous */
 #define gets(str)  fgets((str), sizeof(str), stdin)
@@ -87,6 +89,7 @@ char *usage=
 "       r[n] = read msgs starting at number n\n"
 "       x[n] = dump msg index at number n\n"
 "       v[n] = view msg headers starting at number n\n"
+"       V[n] = view msg headers starting at number n verbose\n"
 "       i[f] = import msg from text file f (or use stdin)\n"
 "       e[f] = import e-mail from text file f (or use stdin)\n"
 "       n[f] = import netmail from text file f (or use stdin)\n"
@@ -332,10 +335,10 @@ void postmsg(char type, char* to, char* to_number, char* to_address,
 		bail(1); 
 	}
 
-	safe_snprintf(str,sizeof(str),"SMBUTIL %s-%s r%s %s %s"
+	safe_snprintf(str,sizeof(str),"SMBUTIL %s-%s %s/%s %s %s"
 		,SMBUTIL_VER
 		,PLATFORM_DESC
-		,revision
+		,GIT_BRANCH, GIT_HASH
 		,__DATE__
 		,compiler
 		);
@@ -490,14 +493,15 @@ void listmsgs(ulong start, ulong count)
 	int i;
 	ulong l=0;
 	smbmsg_t msg;
+	size_t idxreclen = smb_idxreclen(&smb);
 
 	if(!start)
 		start=1;
 	if(!count)
 		count=~0;
-	fseek(smb.sid_fp,(start-1L)*sizeof(idxrec_t),SEEK_SET);
 	while(l<count) {
-		if(!fread(&msg.idx,1,sizeof(idxrec_t),smb.sid_fp))
+		fseek(smb.sid_fp,((start-1L) + l)*idxreclen,SEEK_SET);
+		if(!fread(&msg.idx,1,sizeof(msg.idx),smb.sid_fp))
 			break;
 		i=smb_lockmsghdr(&smb,&msg);
 		if(i) {
@@ -558,25 +562,43 @@ char *my_timestr(time_t intime)
 /****************************************************************************/
 void dumpindex(ulong start, ulong count)
 {
+	char tmp[128];
 	ulong l=0;
 	idxrec_t idx;
+	size_t idxreclen = smb_idxreclen(&smb);
 
 	if(!start)
 		start=1;
 	if(!count)
 		count=~0;
-	fseek(smb.sid_fp,(start-1L)*sizeof(idxrec_t),SEEK_SET);
 	while(l<count) {
+		fseek(smb.sid_fp,((start-1L) + l) * idxreclen,SEEK_SET);
 		if(!fread(&idx,1,sizeof(idx),smb.sid_fp))
 			break;
 		printf("%10"PRIu32"  ", idx.number);
-		if(idx.attr&MSG_VOTE && !(idx.attr&MSG_POLL))
-			printf("V  %04hX  %-10"PRIu32, idx.votes,idx.remsg);
-		else
-			printf("%c  %04hX  %04hX  %04X"
-				,(idx.attr&MSG_POLL_VOTE_MASK) == MSG_POLL_CLOSURE ? 'C' : (idx.attr&MSG_POLL ? 'P':' ')
-				,idx.from, idx.to, idx.subj);
-		printf("  %04X  %06X  %s\n", idx.attr, idx.offset, my_timestr(idx.time));
+		switch(smb_msg_type(idx.attr)) {
+			case SMB_MSG_TYPE_FILE:
+				printf("F %10lu  ", (ulong)idx.size);
+				break;
+			case SMB_MSG_TYPE_BALLOT:
+				printf("V  %04hX  %-10"PRIu32, idx.votes,idx.remsg);
+				break;
+			default:
+				printf("%c  %04hX  %04hX  %04X"
+					,(idx.attr&MSG_POLL_VOTE_MASK) == MSG_POLL_CLOSURE ? 'C' : (idx.attr&MSG_POLL ? 'P':' ')
+					,idx.from, idx.to, idx.subj);
+				break;
+		}
+		printf("  %04X  %06X  %s", idx.attr, idx.offset
+			,xpDate_to_isoDateStr(time_to_xpDate(idx.time), "-", tmp, sizeof(tmp)));
+		if(smb_msg_type(idx.attr) == SMB_MSG_TYPE_FILE && idxreclen == sizeof(fileidxrec_t)) {
+			fileidxrec_t fidx;
+			fseek(smb.sid_fp,((start-1L) + l) * idxreclen,SEEK_SET);
+			if(!fread(&fidx,1,sizeof(fidx),smb.sid_fp))
+				break;
+			printf("  %02X  %.*s", fidx.hash.flags, (int)sizeof(fidx.name), fidx.name);
+		}
+		printf("\n");
 		l++; 
 	}
 }
@@ -589,14 +611,15 @@ void viewmsgs(ulong start, ulong count, BOOL verbose)
 	int i,j;
 	ulong l=0;
 	smbmsg_t msg;
+	size_t idxreclen = smb_idxreclen(&smb);
 
 	if(!start)
 		start=1;
 	if(!count)
 		count=~0;
-	fseek(smb.sid_fp,(start-1L)*sizeof(idxrec_t),SEEK_SET);
 	while(l<count) {
-		if(!fread(&msg.idx,1,sizeof(idxrec_t),smb.sid_fp))
+		fseek(smb.sid_fp,((start-1L) + l) * idxreclen,SEEK_SET);
+		if(!fread(&msg.idx,1,sizeof(msg.idx),smb.sid_fp))
 			break;
 		i=smb_lockmsghdr(&smb,&msg);
 		if(i) {
@@ -613,7 +636,7 @@ void viewmsgs(ulong start, ulong count, BOOL verbose)
 		}
 
 		printf("--------------------\n");
-		printf("%-16.16s %ld\n"		,"index record",ftell(smb.sid_fp)/sizeof(idxrec_t));
+		printf("%-16.16s %ld\n"		,"index record",ftell(smb.sid_fp)/idxreclen);
 		smb_dump_msghdr(stdout,&msg);
 		if(verbose) {
 			for(i=0; i<msg.total_hfields; i++) {
@@ -654,11 +677,13 @@ void dump_hashes(void)
 		printf("%-10s: %s\n",		"Time",		my_timestr(hash.time));
 		printf("%-10s: %02x\n",		"Flags",	hash.flags);
 		if(hash.flags&SMB_HASH_CRC16)
-			printf("%-10s: %04x\n",	"CRC-16",	hash.crc16);
+			printf("%-10s: %04x\n",	"CRC-16",	hash.data.crc16);
 		if(hash.flags&SMB_HASH_CRC32)
-			printf("%-10s: %08"PRIx32"\n","CRC-32",	hash.crc32);
+			printf("%-10s: %08"PRIx32"\n","CRC-32",	hash.data.crc32);
 		if(hash.flags&SMB_HASH_MD5)
-			printf("%-10s: %s\n",	"MD5",		MD5_hex((BYTE*)tmp,hash.md5));
+			printf("%-10s: %s\n",	"MD5",		MD5_hex(tmp,hash.data.md5));
+		if(hash.flags&SMB_HASH_SHA1)
+			printf("%-10s: %s\n",	"SHA-1",	SHA1_hex(tmp,hash.data.sha1));
 	}
 
 	smb_close_hash(&smb);
@@ -675,6 +700,8 @@ void maint(void)
 	time_t now;
 	smbmsg_t msg;
 	idxrec_t *idx;
+	size_t idxreclen = smb_idxreclen(&smb);
+	uint8_t* idxbuf;
 
 	printf("Maintaining %s\r\n",smb.file);
 	now=time(NULL);
@@ -697,7 +724,7 @@ void maint(void)
 
 		printf("Maintaining %s hash file\r\n", smb.file);
 
-		if((smb.status.attr&(SMB_EMAIL|SMB_NOHASH)) == 0) {
+		if((smb.status.attr&(SMB_EMAIL|SMB_NOHASH|SMB_FILE_DIRECTORY)) == 0) {
 			max_hashes = smb.status.max_msgs;
 			if(smb.status.max_crcs > max_hashes)
 				max_hashes = smb.status.max_crcs;
@@ -726,21 +753,22 @@ void maint(void)
 		return; 
 	}
 	printf("Loading index...\n");
-	if((idx=(idxrec_t *)malloc(sizeof(idxrec_t)*smb.status.total_msgs))
+	if((idxbuf = malloc(idxreclen * smb.status.total_msgs))
 		==NULL) {
 		smb_unlocksmbhdr(&smb);
 		fprintf(errfp,"\n%s!Error allocating %" XP_PRIsize_t "u bytes of memory\n"
-			,beep,sizeof(idxrec_t)*smb.status.total_msgs);
+			,beep,idxreclen * smb.status.total_msgs);
 		return; 
 	}
 	fseek(smb.sid_fp,0L,SEEK_SET);
-	l = fread(idx, sizeof(idxrec_t), smb.status.total_msgs, smb.sid_fp);
+	l = fread(idxbuf, idxreclen, smb.status.total_msgs, smb.sid_fp);
 
 	printf("\nDone.\n\n");
 	printf("Scanning for pre-flagged messages...\n");
 	for(m=0;m<l;m++) {
+		idx = (idxrec_t*)(idxbuf + (m * idxreclen));
 //		printf("\r%2lu%%",m ? (long)(100.0/((float)l/m)) : 0);
-		if(idx[m].attr&MSG_DELETE)
+		if(idx->attr&MSG_DELETE)
 			flagged++; 
 	}
 	printf("\r100%% (%lu pre-flagged for deletion)\n",flagged);
@@ -749,14 +777,15 @@ void maint(void)
 		printf("Scanning for messages more than %u days old...\n"
 			,smb.status.max_age);
 		for(m=f=0;m<l;m++) {
+			idx = (idxrec_t*)(idxbuf + (m * idxreclen));
 //			printf("\r%2lu%%",m ? (long)(100.0/((float)l/m)) : 0);
-			if(idx[m].attr&(MSG_PERMANENT|MSG_DELETE))
+			if(idx->attr&(MSG_PERMANENT|MSG_DELETE))
 				continue;
-			if((ulong)now>idx[m].time && (now-idx[m].time)/(24L*60L*60L)
+			if((ulong)now>idx->time && (now-idx->time)/(24L*60L*60L)
 				>smb.status.max_age) {
 				f++;
 				flagged++;
-				idx[m].attr|=MSG_DELETE; 
+				idx->attr|=MSG_DELETE; 
 			} 
 		}  /* mark for deletion */
 		printf("\r100%% (%lu flagged for deletion due to age)\n",f); 
@@ -765,34 +794,36 @@ void maint(void)
 	printf("Scanning for read messages to be killed...\n");
 	uint32_t total_msgs = 0;
 	for(m=f=0;m<l;m++) {
-		enum smb_msg_type type = smb_msg_type(idx[m].attr);
+		idx = (idxrec_t*)(idxbuf + (m * idxreclen));
+		enum smb_msg_type type = smb_msg_type(idx->attr);
 		if(type == SMB_MSG_TYPE_NORMAL || type == SMB_MSG_TYPE_POLL)
 			total_msgs++;
 //		printf("\r%2lu%%",m ? (long)(100.0/((float)l/m)) : 0);
-		if(idx[m].attr&(MSG_PERMANENT|MSG_DELETE))
+		if(idx->attr&(MSG_PERMANENT|MSG_DELETE))
 			continue;
-		if((idx[m].attr&(MSG_READ|MSG_KILLREAD))==(MSG_READ|MSG_KILLREAD)) {
+		if((idx->attr&(MSG_READ|MSG_KILLREAD))==(MSG_READ|MSG_KILLREAD)) {
 			f++;
 			flagged++;
-			idx[m].attr|=MSG_DELETE; 
-		}
+			idx->attr|=MSG_DELETE; 
+		} 
 	}
 	printf("\r100%% (%lu flagged for deletion due to read status)\n",f);
 
 	if(smb.status.max_msgs && total_msgs - flagged > smb.status.max_msgs) {
 		printf("Flagging excess messages for deletion...\n");
-		for(m=n=0,f=flagged; total_msgs - flagged > smb.status.max_msgs && m<l; m++) {
-			if(idx[m].attr&(MSG_PERMANENT|MSG_DELETE))
+		for(m=n=0,f=flagged;l-flagged>smb.status.max_msgs && m<l;m++) {
+			idx = (idxrec_t*)(idxbuf + (m * idxreclen));
+			if(idx->attr&(MSG_PERMANENT|MSG_DELETE))
 				continue;
 			printf("%lu of %lu\r",++n,(total_msgs - f)-smb.status.max_msgs);
 			flagged++;
-			idx[m].attr|=MSG_DELETE; 
+			idx->attr|=MSG_DELETE; 
 		}			/* mark for deletion */
 		printf("\nDone.\n\n"); 
 	}
 
 	if(!flagged) {				/* No messages to delete */
-		free(idx);
+		free(idxbuf);
 		smb_unlocksmbhdr(&smb);
 		return; 
 	}
@@ -818,9 +849,10 @@ void maint(void)
 		}
 
 		for(m=n=0;m<l;m++) {
-			if(idx[m].attr&MSG_DELETE) {
+			idx = (idxrec_t*)(idxbuf + (m * idxreclen));
+			if(idx->attr&MSG_DELETE) {
 				printf("%lu of %lu\r",++n,flagged);
-				msg.idx=idx[m];
+				msg.idx=*idx;
 				msg.hdr.number=msg.idx.number;
 				if((i=smb_getmsgidx(&smb,&msg))!=0) {
 					fprintf(errfp,"\n%s!smb_getmsgidx returned %d: %s\n"
@@ -867,17 +899,18 @@ void maint(void)
 	printf("Re-writing index...\n");
 	rewind(smb.sid_fp);
 	for(m=n=0;m<l;m++) {
-		if(idx[m].attr&MSG_DELETE)
+		idx = (idxrec_t*)(idxbuf + (m * idxreclen));
+		if(idx->attr&MSG_DELETE)
 			continue;
 		n++;
 		printf("%lu of %lu\r", n, l-flagged);
-		fwrite(&idx[m],sizeof(idxrec_t),1,smb.sid_fp); 
+		fwrite(idx, idxreclen ,1 ,smb.sid_fp); 
 	}
 	fflush(smb.sid_fp);
-	CHSIZE_FP(smb.sid_fp, n * sizeof(idxrec_t));
+	CHSIZE_FP(smb.sid_fp, n * idxreclen);
 	printf("\nDone.\n\n");
 
-	free(idx);
+	free(idxbuf);
 	smb.status.total_msgs-=flagged;
 	smb_putstatus(&smb);
 	smb_unlocksmbhdr(&smb);
@@ -896,13 +929,15 @@ void packmsgs(ulong packable)
 	uchar	buf[SDT_BLOCK_LEN],ch;
 	char	fname[MAX_PATH+1],tmpfname[MAX_PATH+1];
 	int i,size;
-	ulong l,m,n,datoffsets=0,length,total;
+	ulong l,m,n,datoffsets=0,total;
+	off_t length;
 	FILE *tmp_sdt,*tmp_shd,*tmp_sid;
 	BOOL		error=FALSE;
 	smbhdr_t	hdr;
 	smbmsg_t	msg;
 	datoffset_t *datoffset=NULL;
 	time_t		now;
+	size_t		idxreclen = smb_idxreclen(&smb);
 
 	now=time(NULL);
 	printf("Packing %s\n",smb.file);
@@ -1104,11 +1139,11 @@ void packmsgs(ulong packable)
 		fread(&ch,1,1,smb.shd_fp);			/* copy additional base header records */
 		fwrite(&ch,1,1,tmp_shd); 
 	}
-	fseek(smb.sid_fp,0L,SEEK_SET);
 	total=0;
 	for(l=0;l<smb.status.total_msgs;l++) {
+		fseek(smb.sid_fp, l * idxreclen,SEEK_SET);
 		printf("%lu of %"PRIu32"\r",l+1,smb.status.total_msgs);
-		if(!fread(&msg.idx,1,sizeof(idxrec_t),smb.sid_fp))
+		if(!fread(&msg.idx, 1, sizeof(msg.idx), smb.sid_fp))
 			break;
 		if(msg.idx.attr&MSG_DELETE) {
 			printf("\nDeleted index %lu: msg number %lu\n", l,(ulong) msg.idx.number);
@@ -1158,8 +1193,8 @@ void packmsgs(ulong packable)
 			}
 
 			if(!(smb.status.attr&SMB_HYPERALLOC)) {
-				datoffset[datoffsets].new=msg.hdr.offset
-					=smb_fallocdat(&smb,m,1);
+				msg.hdr.offset = (uint32_t)smb_fallocdat(&smb,(uint32_t)m,1);
+				datoffset[datoffsets].new = msg.hdr.offset;
 				datoffsets++;
 				fseek(tmp_sdt,msg.hdr.offset,SEEK_SET); 
 			}
@@ -1189,9 +1224,10 @@ void packmsgs(ulong packable)
 		if(smb.status.attr&SMB_HYPERALLOC)
 			msg.idx.offset=ftell(tmp_shd);
 		else
-			msg.idx.offset=smb_fallochdr(&smb,length)+smb.status.header_offset;
+			msg.idx.offset=(uint32_t)smb_fallochdr(&smb,(ulong)length)+smb.status.header_offset;
 		smb_init_idx(&smb, &msg);
-		fwrite(&msg.idx,1,sizeof(idxrec_t),tmp_sid);
+		fseek(tmp_sid, l * idxreclen, SEEK_SET);
+		fwrite(&msg.idx, 1, sizeof(msg.idx), tmp_sid);
 
 		/* Write the new header entry */
 		fseek(tmp_shd,msg.idx.offset,SEEK_SET);
@@ -1376,15 +1412,16 @@ void readmsgs(ulong start, ulong count)
 	int 	i,done=0,domsg=1;
 	ulong	rd = 0;
 	smbmsg_t msg;
+	size_t	idxreclen = smb_idxreclen(&smb);
 
 	if(start)
-		msg.offset=start-1;
+		msg.idx_offset=start-1;
 	else
-		msg.offset=0;
+		msg.idx_offset=0;
 	while(!done) {
 		if(domsg) {
-			fseek(smb.sid_fp,msg.offset*sizeof(idxrec_t),SEEK_SET);
-			if(!fread(&msg.idx,1,sizeof(idxrec_t),smb.sid_fp))
+			fseek(smb.sid_fp,msg.idx_offset*idxreclen,SEEK_SET);
+			if(!fread(&msg.idx,1,sizeof(msg.idx),smb.sid_fp))
 				break;
 			i=smb_lockmsghdr(&smb,&msg);
 			if(i) {
@@ -1400,12 +1437,14 @@ void readmsgs(ulong start, ulong count)
 				break; 
 			}
 
-			printf("\n#%"PRIu32" (%d)\n",msg.hdr.number,msg.offset+1);
+			printf("\n#%"PRIu32" (%d)\n",msg.hdr.number,msg.idx_offset+1);
 			printf("Subj : %s\n",msg.subj);
-			printf("Attr : %04hX\n", msg.hdr.attr);
-			printf("To   : %s",msg.to);
-			if(msg.to_net.type)
-				printf(" (%s)",smb_netaddr(&msg.to_net));
+			printf("Attr : %04hX", msg.hdr.attr);
+			if(*msg.to) {
+				printf("\nTo   : %s",msg.to);
+				if(msg.to_net.type)
+					printf(" (%s)",smb_netaddr(&msg.to_net));
+			}
 			printf("\nFrom : %s",msg.from);
 			if(msg.from_net.type)
 				printf(" (%s)",smb_netaddr(&msg.from_net));
@@ -1413,7 +1452,7 @@ void readmsgs(ulong start, ulong count)
 				,my_timestr(msg.hdr.when_written.time)
 				,smb_zonestr(msg.hdr.when_written.zone,NULL));
 
-			printf("\n\n");
+			printf("\n%s\n", msg.summary ? msg.summary : "");
 
 			if((inbuf=smb_getmsgtxt(&smb,&msg, msgtxtmode))!=NULL) {
 				char* p;
@@ -1435,7 +1474,7 @@ void readmsgs(ulong start, ulong count)
 		if(count) {
 			if(rd >= count)
 				break;
-			msg.offset++;
+			msg.idx_offset++;
 			continue;
 		}
 		printf("\nReading %s (?=Menu): ",smb.file);
@@ -1462,13 +1501,13 @@ void readmsgs(ulong start, ulong count)
 				break;
 			case '-':
 				printf("Backwards\n");
-				if(msg.offset)
-					msg.offset--;
+				if(msg.idx_offset)
+					msg.idx_offset--;
 				break;
 			case 'T':
 				printf("Ten titles\n");
-				listmsgs(msg.offset+2,10);
-				msg.offset+=10;
+				listmsgs(msg.idx_offset+2,10);
+				msg.idx_offset+=10;
 				domsg=0;
 				break;
 			case 'L':
@@ -1489,7 +1528,7 @@ void readmsgs(ulong start, ulong count)
 			case '\n':
 			case '+':
 				printf("Next\n");
-				msg.offset++;
+				msg.idx_offset++;
 				break; 
 		} 
 	}
@@ -1548,7 +1587,7 @@ long getmsgnum(const char* str)
 		msg.hdr.number = atol(str + 1);
 		int result = smb_getmsgidx(&smb, &msg);
 		if(result == SMB_SUCCESS)
-			return msg.offset + 1;
+			return msg.idx_offset + 1;
 	}
 	return atol(str);
 }
@@ -1586,19 +1625,30 @@ int main(int argc, char **argv)
 	else	/* if redirected, don't send status messages to stderr */
 		statfp=nulfp;
 
-	sscanf("$Revision: 1.136 $", "%*s %s", revision);
-
 	DESCRIBE_COMPILER(compiler);
 
 	smb.file[0]=0;
-	fprintf(statfp,"\nSMBUTIL v%s-%s (rev %s) SMBLIB %s - Synchronet Message Base "\
+	fprintf(statfp,"\nSMBUTIL v%s-%s %s/%s SMBLIB %s - Synchronet Message Base "\
 		"Utility\n\n"
 		,SMBUTIL_VER
 		,PLATFORM_DESC
-		,revision
+		,GIT_BRANCH, GIT_HASH
 		,smb_lib_ver()
 		);
 
+	if(sizeof(hash_t) != SIZEOF_SMB_HASH_T) {
+		printf("!Size of hash_t unexpected: %d\n", (int)sizeof(hash_t));
+		return EXIT_FAILURE;
+	}
+	if(sizeof(idxrec_t) != SIZEOF_SMB_IDXREC_T) {
+		printf("!Size of idxrec_t unexpected: %d\n", (int)sizeof(idxrec_t));
+		return EXIT_FAILURE;
+	}
+	if(sizeof(fileidxrec_t) != SIZEOF_SMB_FILEIDXREC_T) {
+		printf("!Size of fileidxrec_t unexpected: %d\n", (int)sizeof(fileidxrec_t));
+		return EXIT_FAILURE;
+	}
+
 	/* Automatically detect the system time zone (if possible) */
 	tzset();
 	now=time(NULL);
@@ -1704,7 +1754,7 @@ int main(int argc, char **argv)
 						beep="\a";
 						break;
 					default:
-						printf("\nUnknown opt '%c'\n",argv[x][j]);
+						fprintf(stderr, "\nUnknown opt '%c'\n", argv[x][j]);
 					case '?':
 						printf("%s",usage);
 						bail(1);
@@ -1819,14 +1869,14 @@ int main(int argc, char **argv)
 							y=strlen(cmd)-1;
 							break;
 						case 'R':
-							printf("Re-initialzing %s SMB/status header\n", smb.file);
+							printf("Re-initializing %s SMB/status header\n", smb.file);
 							if((i=smb_initsmbhdr(&smb)) != SMB_SUCCESS) {
 								fprintf(errfp, "\n%s!error %d: %s\n", beep, i, smb.last_error);
 								return i;
 							}
 							memset(&smb.status, 0, sizeof(smb.status));
 							smb.status.header_offset = sizeof(smbhdr_t) + sizeof(smb.status);
-							smb.status.total_msgs = filelength(fileno(smb.sid_fp)) / sizeof(idxrec_t);
+							smb.status.total_msgs = (uint32_t)filelength(fileno(smb.sid_fp)) / smb_idxreclen(&smb);
 							idxrec_t idx;
 							if((i=smb_getlastidx(&smb, &idx)) != SMB_SUCCESS) {
 								fprintf(errfp, "\n%s!error %d: %s\n", beep, i, smb.last_error);
diff --git a/src/sbbs3/sortdir.cpp b/src/sbbs3/sortdir.cpp
deleted file mode 100644
index 01af61addf197875ba39822b41f72f2a291924d9..0000000000000000000000000000000000000000
--- a/src/sbbs3/sortdir.cpp
+++ /dev/null
@@ -1,239 +0,0 @@
-/* sortdir.cpp */
-
-/* Synchronet file database sorting routines */
-
-/* $Id: sortdir.cpp,v 1.8 2018/02/20 05:21:04 rswindell Exp $ */
-
-/****************************************************************************
- * @format.tab-size 4		(Plain Text/Source Code File Header)			*
- * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
- *																			*
- * Copyright Rob Swindell - http://www.synchro.net/copyright.html			*
- *																			*
- * 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.					*
- * See the GNU General Public License for more details: gpl.txt or			*
- * http://www.fsf.org/copyleft/gpl.html										*
- *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
- *																			*
- * For Synchronet coding style and modification guidelines, see				*
- * http://www.synchro.net/source.html										*
- *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
- * Note: If this box doesn't appear square, then you need to fix your tabs.	*
- ****************************************************************************/
-
-#include "sbbs.h"
-
-/****************************************************************************/
-/* Re-sorts file directory 'dirnum' according to dir[dirnum]->sort type     */
-/****************************************************************************/
-void sbbs_t::resort(uint dirnum)
-{
-	char	str[25],ixbfname[128],datfname[128],exbfname[128],txbfname[128]
-			,ext[512],nulbuf[512];
-	char 	tmp[512];
-	uchar*	ixbbuf, *datbuf;
-	uchar*	ixbptr[MAX_FILES];
-	int		ixbfile,datfile,exbfile,txbfile,i,j;
-	long	ixblen,datlen,offset,newoffset,l;
-
-	memset(nulbuf,0,512);
-	bprintf(text[ResortLineFmt],cfg.lib[cfg.dir[dirnum]->lib]->sname,cfg.dir[dirnum]->sname);
-	sprintf(ixbfname,"%s%s.ixb",cfg.dir[dirnum]->data_dir,cfg.dir[dirnum]->code);
-	sprintf(datfname,"%s%s.dat",cfg.dir[dirnum]->data_dir,cfg.dir[dirnum]->code);
-	sprintf(exbfname,"%s%s.exb",cfg.dir[dirnum]->data_dir,cfg.dir[dirnum]->code);
-	sprintf(txbfname,"%s%s.txb",cfg.dir[dirnum]->data_dir,cfg.dir[dirnum]->code);
-
-	if(flength(ixbfname)<1L || flength(datfname)<1L) {
-		remove(exbfname);
-		remove(txbfname);
-		remove(ixbfname);
-		remove(datfname);
-		bputs(text[ResortEmptyDir]);
-		return; }
-	bputs(text[Sorting]);
-	if((ixbfile=nopen(ixbfname,O_RDONLY))==-1) {
-		errormsg(WHERE,ERR_OPEN,ixbfname,O_RDONLY);
-		return; }
-	if((datfile=nopen(datfname,O_RDONLY))==-1) {
-		close(ixbfile);
-		errormsg(WHERE,ERR_OPEN,datfname,O_RDONLY);
-		return; }
-	ixblen=(long)filelength(ixbfile);
-	datlen=(long)filelength(datfile);
-	if((ixbbuf=(uchar *)malloc(ixblen))==NULL) {
-		close(ixbfile);
-		close(datfile);
-		errormsg(WHERE,ERR_ALLOC,ixbfname,ixblen);
-		return; }
-	if((datbuf=(uchar *)malloc(datlen))==NULL) {
-		close(ixbfile);
-		close(datfile);
-		free((char *)ixbbuf);
-		errormsg(WHERE,ERR_ALLOC,datfname,datlen);
-		return; }
-	if(lread(ixbfile,ixbbuf,ixblen)!=ixblen) {
-		close(ixbfile);
-		close(datfile);
-		free((char *)ixbbuf);
-		free((char *)datbuf);
-		errormsg(WHERE,ERR_READ,ixbfname,ixblen);
-		return; }
-	if(lread(datfile,datbuf,datlen)!=datlen) {
-		close(ixbfile);
-		close(datfile);
-		free((char *)ixbbuf);
-		free((char *)datbuf);
-		errormsg(WHERE,ERR_READ,datfname,datlen);
-		return; }
-	close(ixbfile);
-	close(datfile);
-	if((ixbfile=nopen(ixbfname,O_WRONLY|O_TRUNC))==-1) {
-		free((char *)ixbbuf);
-		free((char *)datbuf);
-		errormsg(WHERE,ERR_OPEN,ixbfname,O_WRONLY|O_TRUNC);
-		return; }
-	if((datfile=nopen(datfname,O_WRONLY|O_TRUNC))==-1) {
-		close(ixbfile);
-		free((char *)ixbbuf);
-		free((char *)datbuf);
-		errormsg(WHERE,ERR_OPEN,datfname,O_WRONLY|O_TRUNC);
-		return; }
-	for(l=0,i=0;l<ixblen && i<MAX_FILES;l+=F_IXBSIZE,i++)
-		ixbptr[i]=ixbbuf+l;
-	switch(cfg.dir[dirnum]->sort) {
-		case SORT_NAME_A:
-			qsort((void *)ixbptr,ixblen/F_IXBSIZE,sizeof(ixbptr[0])
-				,(int(*)(const void*, const void*))fnamecmp_a);
-			break;
-		case SORT_NAME_D:
-			qsort((void *)ixbptr,ixblen/F_IXBSIZE,sizeof(ixbptr[0])
-				,(int(*)(const void*, const void*))fnamecmp_d);
-			break;
-		case SORT_DATE_A:
-			qsort((void *)ixbptr,ixblen/F_IXBSIZE,sizeof(ixbptr[0])
-				,(int(*)(const void*, const void*))fdatecmp_a);
-			break;
-		case SORT_DATE_D:
-			qsort((void *)ixbptr,ixblen/F_IXBSIZE,sizeof(ixbptr[0])
-				,(int(*)(const void*, const void*))fdatecmp_d);
-			break; }
-
-	if((exbfile=nopen(exbfname,O_RDWR|O_CREAT))==-1) {
-		close(ixbfile);
-		close(datfile);
-		free((char *)ixbbuf);
-		free((char *)datbuf);
-		errormsg(WHERE,ERR_OPEN,exbfname,O_RDWR|O_CREAT);
-		return; }
-	if((txbfile=nopen(txbfname,O_RDWR|O_CREAT))==-1) {
-		close(ixbfile);
-		close(datfile);
-		close(exbfile);
-		free((char *)ixbbuf);
-		free((char *)datbuf);
-		errormsg(WHERE,ERR_OPEN,txbfname,O_RDWR|O_CREAT);
-		return; }
-
-	for(i=0;i<ixblen/F_IXBSIZE;i++) {
-		offset=ixbptr[i][11]|((long)ixbptr[i][12]<<8)|((long)ixbptr[i][13]<<16);
-		lwrite(datfile,&datbuf[offset],F_LEN);
-
-		newoffset=(ulong)i*(ulong)F_LEN;
-
-		j=datbuf[offset+F_MISC];  /* misc bits */
-		if(j!=ETX) j-=' ';
-		if(j&FM_EXTDESC) { /* extended description */
-			lseek(exbfile,(offset/F_LEN)*512L,SEEK_SET);
-			memset(ext,0,512);
-			read(exbfile,ext,512);
-			while(filelength(txbfile)<(newoffset/F_LEN)*512L) {
-	//			  lseek(txbfile,0L,SEEK_END);
-				write(txbfile,nulbuf,512); }
-			lseek(txbfile,(newoffset/F_LEN)*512L,SEEK_SET);
-			write(txbfile,ext,512); }
-
-		str[0]=newoffset&0xff;	   /* Get offset within DAT file for IXB file */
-		str[1]=(newoffset>>8)&0xff;
-		str[2]=(newoffset>>16)&0xff;
-		lwrite(ixbfile,ixbptr[i],11);       /* filename */
-		lwrite(ixbfile,str,3);              /* offset */
-		lwrite(ixbfile,ixbptr[i]+14,8); }   /* upload and download times */
-	close(exbfile);
-	close(txbfile);
-	close(ixbfile);
-	close(datfile);
-	remove(exbfname);
-	rename(txbfname,exbfname);
-	if(!flength(exbfname))
-		remove(exbfname);
-	free((char *)ixbbuf);
-	free((char *)datbuf);
-	if(ixblen/F_IXBSIZE==datlen/F_LEN)
-		bputs(text[Sorted]);
-	else
-		bprintf(text[Compressed]
-			,(uint)((datlen/F_LEN)-(ixblen/F_IXBSIZE))
-			,ultoac(((datlen/F_LEN)-(ixblen/F_IXBSIZE))*F_LEN,tmp));
-}
-
-/****************************************************************************/
-/* Compares filenames for ascending name sort								*/
-/****************************************************************************/
-int fnamecmp_a(char **str1, char **str2)
-{
-	return(strnicmp(*str1,*str2,11));
-}
-
-/****************************************************************************/
-/* Compares filenames for descending name sort								*/
-/****************************************************************************/
-int fnamecmp_d(char **str1, char **str2)
-{
-	return(strnicmp(*str2,*str1,11));
-}
-
-/****************************************************************************/
-/* Compares file upload dates for ascending date sort						*/
-/****************************************************************************/
-int fdatecmp_a(uchar **buf1, uchar **buf2)
-{
-	time_t date1,date2;
-
-	date1=((*buf1)[14]|((long)(*buf1)[15]<<8)|((long)(*buf1)[16]<<16)
-		|((long)(*buf1)[17]<<24));
-	date2=((*buf2)[14]|((long)(*buf2)[15]<<8)|((long)(*buf2)[16]<<16)
-		|((long)(*buf2)[17]<<24));
-	if(date1>date2)	return(1);
-	if(date1<date2)	return(-1);
-	return(0);
-}
-
-/****************************************************************************/
-/* Compares file upload dates for descending date sort						*/
-/****************************************************************************/
-int fdatecmp_d(uchar **buf1, uchar **buf2)
-{
-	time_t date1,date2;
-
-	date1=((*buf1)[14]|((long)(*buf1)[15]<<8)|((long)(*buf1)[16]<<16)
-		|((long)(*buf1)[17]<<24));
-	date2=((*buf2)[14]|((long)(*buf2)[15]<<8)|((long)(*buf2)[16]<<16)
-		|((long)(*buf2)[17]<<24));
-	if(date1>date2)	return(-1);
-	if(date1<date2)	return(1);
-	return(0);
-}
diff --git a/src/sbbs3/str.cpp b/src/sbbs3/str.cpp
index 7f2ae5bd18b1f3f534f73a3cf1189083036cc8a8..d9b29a8598908369d22c5d8590deaf2ecb803c9a 100644
--- a/src/sbbs3/str.cpp
+++ b/src/sbbs3/str.cpp
@@ -777,7 +777,8 @@ void sbbs_t::subinfo(uint subnum)
 	bprintf(text[SubInfoLongName],cfg.sub[subnum]->lname);
 	bprintf(text[SubInfoShortName],cfg.sub[subnum]->sname);
 	bprintf(text[SubInfoQWKName],cfg.sub[subnum]->qwkname);
-	bprintf(text[SubInfoMaxMsgs],cfg.sub[subnum]->maxmsgs);
+	if(cfg.sub[subnum]->maxmsgs)
+		bprintf(text[SubInfoMaxMsgs],cfg.sub[subnum]->maxmsgs);
 	if(cfg.sub[subnum]->misc&SUB_QNET)
 		bprintf(text[SubInfoTagLine],cfg.sub[subnum]->tagline);
 	if(cfg.sub[subnum]->misc&SUB_FIDO)
@@ -801,7 +802,8 @@ void sbbs_t::dirinfo(uint dirnum)
 	bprintf(text[DirInfoShortName],cfg.dir[dirnum]->sname);
 	if(cfg.dir[dirnum]->exts[0])
 		bprintf(text[DirInfoAllowedExts],cfg.dir[dirnum]->exts);
-	bprintf(text[DirInfoMaxFiles],cfg.dir[dirnum]->maxfiles);
+	if(cfg.dir[dirnum]->maxfiles)
+		bprintf(text[DirInfoMaxFiles],cfg.dir[dirnum]->maxfiles);
 	SAFEPRINTF2(str,"%s%s.msg",cfg.dir[dirnum]->data_dir,cfg.dir[dirnum]->code);
 	if(fexist(str) && yesno(text[DirInfoViewFileQ]))
 		printfile(str,0);
@@ -950,8 +952,7 @@ void sbbs_t::xfer_prot_menu(enum XFER_TYPE type)
 	if(menu(prot_menu_file[type], P_NOERROR)) {
 		return;
 	}
-
-	CRLF;
+	cond_blankline();
 	int printed=0;
 	for(int i=0;i<cfg.total_prots;i++) {
 		if(!chk_ar(cfg.prot[i]->ar,&useron,&client))
@@ -964,14 +965,12 @@ void sbbs_t::xfer_prot_menu(enum XFER_TYPE type)
 			continue;
 		if(type==XFER_BATCH_DOWNLOAD && cfg.prot[i]->batdlcmd[0]==0)
 			continue;
-		if(type==XFER_BIDIR && cfg.prot[i]->bicmd[0]==0)
-			continue;
 		if(printed && (cols < 80 || (printed%2)==0))
 			CRLF;
 		bprintf(text[TransferProtLstFmt],cfg.prot[i]->mnemonic,cfg.prot[i]->name);
 		printed++;
 	}
-	CRLF;
+	newline();
 }
 
 void sbbs_t::node_stats(uint node_num)
diff --git a/src/sbbs3/str_util.c b/src/sbbs3/str_util.c
index 829c5d13d88bc881d708fe20d1653adcd484a5d7..3deaaa6803df78203e1bb9643fc0a0b2f5ecb0a0 100644
--- a/src/sbbs3/str_util.c
+++ b/src/sbbs3/str_util.c
@@ -135,34 +135,6 @@ char* strip_char(const char* str, char* dest, char ch)
 	return retval;
 }
 
-char* prep_file_desc(const char *str, char* dest)
-{
-	int	i,j;
-
-	if(dest==NULL && (dest=strdup(str))==NULL)
-		return NULL;
-	strip_ansi(dest);
-	for(i=j=0;str[i];i++)
-		if(str[i]==CTRL_A && str[i+1]!=0) {
-			i++;
-			if(str[i]==0 || str[i]=='Z')	/* EOF */
-				break;
-			/* convert non-destructive backspace to a destructive backspace */
-			if(str[i]=='<' && j)	
-				j--;
-		}
-		else if(j && str[i]<=' ' && dest[j-1]==' ')
-			continue;
-		else if(i && !IS_ALPHANUMERIC(str[i]) && str[i]==str[i-1])
-			continue;
-		else if((uchar)str[i]>=' ')
-			dest[j++]=str[i];
-		else if(str[i]==TAB || (str[i]==CR && str[i+1]==LF))
-			dest[j++]=' ';
-	dest[j]=0;
-	return dest;
-}
-
 /****************************************************************************/
 /* Pattern matching string search of 'insearchof' in 'string'.				*/
 /****************************************************************************/
@@ -830,6 +802,21 @@ char* subnewsgroupname(scfg_t* cfg, sub_t* sub, char* str, size_t size)
 	return str;
 }
 
+char* dir_area_tag(scfg_t* cfg, dir_t* dir, char* str, size_t size)
+{
+	char* p;
+
+	memset(str, 0, size);
+	if(dir->area_tag[0])
+		strncpy(str, dir->area_tag, size - 1);
+	else {
+		strncpy(str, dir->sname, size - 1);
+		REPLACE_CHARS(str, ' ', '_', p);
+		strupr(str);
+	}
+	return str;
+}
+
 char* get_ctrl_dir(BOOL warn)
 {
 	char* p = getenv("SBBSCTRL");
diff --git a/src/sbbs3/str_util.h b/src/sbbs3/str_util.h
index 987f3d9269dc2f296095c7009978924c8d9cc103..a09cdd3853f5912ada1fd0d97fe9d92b99202e0b 100644
--- a/src/sbbs3/str_util.h
+++ b/src/sbbs3/str_util.h
@@ -55,7 +55,6 @@ DLLEXPORT str_list_t trashcan_list(scfg_t* cfg, const char* name);
 DLLEXPORT char *	strip_ansi(char* str);
 DLLEXPORT char *	strip_exascii(const char *str, char* dest);
 DLLEXPORT char *	strip_space(const char *str, char* dest);
-DLLEXPORT char *	prep_file_desc(const char *str, char* dest);
 DLLEXPORT char *	strip_ctrl(const char *str, char* dest);
 DLLEXPORT char *	strip_char(const char* str, char* dest, char);
 DLLEXPORT char *	net_addr(net_t* net);
@@ -69,6 +68,7 @@ DLLEXPORT BOOL		str_has_ctrl(const char*);
 DLLEXPORT BOOL		str_is_ascii(const char*);
 DLLEXPORT char *	utf8_to_cp437_str(char* str);
 DLLEXPORT char *	subnewsgroupname(scfg_t*, sub_t*, char*, size_t);
+DLLEXPORT char *	dir_area_tag(scfg_t*, dir_t*, char*, size_t);
 DLLEXPORT char * 	get_ctrl_dir(BOOL warn);
 
 #ifdef __cplusplus
diff --git a/src/sbbs3/targets.mk b/src/sbbs3/targets.mk
index 43a3aef500d2e3c7b67bccc0d0f0b9e81159115b..27329c90ae5cdc6e3c1be685348db0479a739901 100644
--- a/src/sbbs3/targets.mk
+++ b/src/sbbs3/targets.mk
@@ -40,6 +40,7 @@ READSAUCE	= $(EXEODIR)$(DIRSEP)readsauce$(EXEFILE)
 SHOWSTAT	= $(EXEODIR)$(DIRSEP)showstat$(EXEFILE)
 PKTDUMP		= $(EXEODIR)$(DIRSEP)pktdump$(EXEFILE)
 FMSGDUMP	= $(EXEODIR)$(DIRSEP)fmsgdump$(EXEFILE)
+UPGRADE_TO_V319 = $(EXEODIR)$(DIRSEP)upgrade_to_v319$(EXEFILE)
 
 UTILS		= $(FIXSMB) $(CHKSMB) \
 			  $(SMBUTIL) $(BAJA) $(NODE) \
@@ -49,7 +50,7 @@ UTILS		= $(FIXSMB) $(CHKSMB) \
 			  $(QWKNODES) $(SLOG) $(ALLUSERS) \
 			  $(DELFILES) $(DUPEFIND) $(SMBACTIV) \
 			  $(SEXYZ) $(DSTSEDIT) $(READSAUCE) $(SHOWSTAT) \
-			  $(PKTDUMP) $(FMSGDUMP)
+			  $(PKTDUMP) $(FMSGDUMP) $(UPGRADE_TO_V319)
 
 GIT_INFO	= git_hash.h git_branch.h
 
@@ -145,7 +146,7 @@ jsdoor: $(GIT_INFO) $(JS_DEPS) $(CRYPT_DEPS) $(XPDEV-MT_LIB) $(SMBLIB) $(UIFCLIB
 
 # Library dependencies
 $(SBBS):
-$(FTPSRVR): 
+$(FTPSRVR): $(SMBLIB) 
 $(WEBSRVR):
 $(MAILSRVR):
 $(SERVICES): 
@@ -161,8 +162,8 @@ $(CHKSMB): $(XPDEV_LIB) $(SMBLIB)
 $(SMBUTIL): $(XPDEV_LIB) $(SMBLIB)
 $(SBBSECHO): $(XPDEV_LIB) $(SMBLIB)
 $(ECHOCFG): $(XPDEV-MT_LIB) $(SMBLIB) $(UIFCLIB-MT) $(CIOLIB-MT)
-$(ADDFILES): $(XPDEV_LIB)
-$(FILELIST): $(XPDEV_LIB)
+$(ADDFILES): $(XPDEV_LIB) $(SMBLIB)
+$(FILELIST): $(XPDEV_LIB) $(SMBLIB)
 $(MAKEUSER): $(XPDEV_LIB)
 $(ANS2ASC):
 $(ASC2ANS):
@@ -170,9 +171,11 @@ $(SEXYZ): $(XPDEV-MT_LIB) $(SMBLIB)
 $(QWKNODES): $(XPDEV_LIB)
 $(SLOG): $(XPDEV_LIB)
 $(ALLUSERS): $(XPDEV_LIB)
-$(DELFILES): $(XPDEV_LIB)
+$(DELFILES): $(XPDEV_LIB) $(SMBLIB)
 $(DUPEFIND): $(XPDEV_LIB) $(SMBLIB)
 $(SMBACTIV): $(XPDEV_LIB) $(SMBLIB)
 $(DSTSEDIT): $(XPDEV_LIB)
 $(READSAUCE): $(XPDEV_LIB)
 $(SHOWSTAT): $(XPDEV_LIB)
+$(UPGRADE_TO_V319): $(XPDEV_LIB) $(SMBLIB)
+
diff --git a/src/sbbs3/text.h b/src/sbbs3/text.h
index fa0e685e316028cd88e9e8888375be01317aaed6..1b0cef1adede0031361edff59b4f079b6239f2c8 100644
--- a/src/sbbs3/text.h
+++ b/src/sbbs3/text.h
@@ -270,7 +270,7 @@ enum {
 	,EditCreditValue
 	,EditTimesDownloaded
 	,EditOpenCount
-	,EditAltPath
+	,Unused260
 	,YouOnlyHaveNCredits
 	,NotEnoughCredits
 	,NotEnoughTimeToDl
@@ -307,12 +307,12 @@ enum {
 	,TempFileInfo
 	,TempDirTotal
 	,NFilesRemoved
-	,ResortWarning
-	,ResortLineFmt
-	,ResortEmptyDir
-	,Sorting
-	,Sorted
-	,Compressed
+	,TagFileQ
+	,TagFilePrompt
+	,Unused299
+	,Unused300
+	,Unused301
+	,Unused302
 	,FileAlreadyInQueue
 	,FileIsNotOnline
 	,FileAddedToBatDlQueue
@@ -336,9 +336,9 @@ enum {
 	,FiDateDled
 	,FiTimesDled
 	,FiTransferTime
-	,FiAlternatePath
-	,InvalidAlternatePathN
-	,FileIsOpen
+	,FiTags
+	,Unused327
+	,FiChecksum
 	,HappyBirthday
 	,TimeToChangePw
 	,NewPasswordQ
@@ -605,7 +605,7 @@ enum {
 	,CreditedAccount
 	,ANSICaptureIsNow
 	,RetrievingFile
-	,AltULPathIsNow
+	,Unused595
 	,PrivatePostQ
 	,PostTo
 	,NoToUser
diff --git a/src/sbbs3/text_defaults.c b/src/sbbs3/text_defaults.c
index 99e587ddac5a6046a1b8fb72ac9c28a308c3e9ba..9c56f57b440f509d937299a4f0d9850a049ae459 100644
--- a/src/sbbs3/text_defaults.c
+++ b/src/sbbs3/text_defaults.c
@@ -132,7 +132,7 @@ const char * const text_defaults[TOTAL_TEXT]={
 	,"\x0d\x0a\x59\x6f\x75\x20\x64\x69\x64\x6e\x27\x74\x20\x70\x6f\x73\x74\x20\x6d\x65\x73\x73\x61\x67\x65\x20\x23\x25\x64\x0d\x0a" // 072 YouDidntPostMsgN
 	,"\x01\x3f\x44\x65\x6c\x65\x74\x65\x20\x6d\x65\x73\x73\x61\x67\x65\x20\x23\x25\x75\x20\x27\x25\x73\x27" // 073 DeletePostQ
 	,"\x01\x6e\x01\x62\x5b\x01\x68\x01\x77\x49\x01\x6e\x01\x62\x5d\x20\x01\x68\x41\x75\x74\x6f\x4c\x6f\x67\x6f\x6e\x20\x76\x69\x61\x20"
-		"\x49\x50\x20\x61\x64\x64\x72\x65\x73\x73\x20\x20\x20\x20\x20\x20\x01\x6e\x01\x62\x3a\x20\x01\x63\x25\x73\x0d\x0a" // 074 UserDefaultsAutoLogon
+		"\x49\x50\x20\x61\x64\x64\x72\x65\x73\x73\x20\x20\x20\x20\x20\x01\x6e\x01\x62\x3a\x20\x01\x63\x25\x73\x0d\x0a" // 074 UserDefaultsAutoLogon
 	,"\x01\x6e\x0d\x0a\x01\x6d\x25\x73\x20\x73\x65\x6e\x74\x20\x74\x6f\x20\x01\x68\x25\x73\x20\x23\x25\x75\x0d\x0a" // 075 MsgSentToUser
 	,"\x01\x5f\x0d\x0a\x01\x79\x01\x68\x54\x65\x78\x74\x20\x74\x6f\x20\x73\x65\x61\x72\x63\x68\x20\x66\x6f\x72\x3a\x20" // 076 SearchStringPrompt
 	,"\x01\x77\x01\x68\xc4\xc4\xc4\xc4\xc4\x5b\x01\x69\x01\x72\x25\x63\x01\x6e\x01\x68\x5d\xc4\xc4\xc4\xc4\xb4\x20\x01\x79\x50\x72\x69"
@@ -270,10 +270,10 @@ const char * const text_defaults[TOTAL_TEXT]={
 	,"\x01\x6e\x01\x67\x07\x54\x65\x6c\x65\x67\x72\x61\x6d\x20\x66\x72\x6f\x6d\x20\x01\x6e\x01\x68\x25\x73\x01\x6e\x01\x67\x20\x6f\x6e"
 		"\x20\x25\x73\x3a\x0d\x0a\x01\x68" // 164 TelegramFmt
 	,"\x0d\x0a\x0d\x0a\x59\x6f\x75\x20\x63\x61\x6e\x27\x74\x20\x64\x6f\x77\x6e\x6c\x6f\x61\x64\x2e\x0d\x0a" // 165 R_Download
-	,"\x0d\x0a\x01\x77\x01\x68\x53\x65\x61\x72\x63\x68\x69\x6e\x67\x20\x61\x6c\x6c\x20\x64\x69\x72\x65\x63\x74\x6f\x72\x69\x65\x73\x20"
-		"\x40\x45\x4c\x4c\x49\x50\x53\x49\x53\x40\x0d\x0a" // 166 SearchingAllDirs
+	,"\x0d\x0a\x01\x77\x01\x68\x53\x65\x61\x72\x63\x68\x69\x6e\x67\x20\x63\x75\x72\x72\x65\x6e\x74\x20\x6c\x69\x62\x72\x61\x72\x79\x20"
+		"\x40\x45\x4c\x4c\x49\x50\x53\x49\x53\x40\x0d\x0a\x01\x71" // 166 SearchingAllDirs
 	,"\x01\x77\x01\x68\x53\x65\x61\x72\x63\x68\x69\x6e\x67\x20\x61\x6c\x6c\x20\x6c\x69\x62\x72\x61\x72\x69\x65\x73\x20\x40\x45\x4c\x4c"
-		"\x49\x50\x53\x49\x53\x40\x0d\x0a" // 167 SearchingAllLibs
+		"\x49\x50\x53\x49\x53\x40\x0d\x0a\x01\x71" // 167 SearchingAllLibs
 	,"\x0d\x0a\x01\x77\x01\x68\x25\x75\x20\x46\x69\x6c\x65\x73\x20\x4c\x69\x73\x74\x65\x64\x2e\x0d\x0a" // 168 NFilesListed
 	,"\x0d\x0a\x01\x77\x01\x68\x45\x6d\x70\x74\x79\x20\x64\x69\x72\x65\x63\x74\x6f\x72\x79\x2e\x0d\x0a" // 169 EmptyDir
 	,"\x0d\x0a\x01\x6e\x01\x63\x53\x65\x61\x72\x63\x68\x69\x6e\x67\x20\x66\x6f\x72\x20\x66\x69\x6c\x65\x73\x20\x75\x70\x6c\x6f\x61\x64"
@@ -376,8 +376,7 @@ const char * const text_defaults[TOTAL_TEXT]={
 	,"\x0d\x0a\x01\x6e\x01\x6d\x01\x68\x25\x73\x20\x01\x6e\x01\x6d\x61\x64\x64\x65\x64\x20\x74\x6f\x20\x62\x61\x74\x63\x68\x20\x75\x70"
 		"\x6c\x6f\x61\x64\x20\x71\x75\x65\x75\x65\x01\x63\x20\x2d\x20\x46\x69\x6c\x65\x73\x3a\x20\x01\x68\x25\x75\x20\x01\x6e\x01\x63\x28"
 		"\x01\x68\x25\x75\x01\x6e\x01\x63\x20\x4d\x61\x78\x29\x0d\x0a" // 230 FileAddedToUlQueue
-	,"\x07\x01\x5f\x01\x77\x01\x68\x4e\x6f\x64\x65\x20\x25\x32\x64\x3a\x20\x01\x67\x25\x73\x01\x6e\x01\x67\x20\x73\x65\x6e\x74\x20\x79"
-		"\x6f\x75\x20\x61\x20\x66\x69\x6c\x65\x2e\x0d\x0a" // 231 UserToUserXferNodeMsg
+	,"\x55\x6e\x75\x73\x65\x64\x20\x32\x33\x31" // 231 UserToUserXferNodeMsg
 	,"\x01\x6e\x01\x3f\x01\x67\x01\x68\x25\x73\x01\x79\x3a\x20\x01\x77\x7e\x42\x01\x79\x61\x74\x63\x68\x20\x64\x6f\x77\x6e\x6c\x6f\x61"
 		"\x64\x2c\x20\x01\x77\x7e\x45\x01\x79\x78\x74\x65\x6e\x64\x65\x64\x20\x69\x6e\x66\x6f\x2c\x20\x01\x77\x7e\x56\x01\x79\x69\x65\x77"
 		"\x20\x66\x69\x6c\x65\x2c\x20\x01\x77\x7e\x51\x01\x79\x75\x69\x74\x20\x6f\x72\x20\x5b\x7e\x4e\x65\x78\x74\x5d\x3a\x20\x01\x77" // 232 FileInfoPrompt
@@ -416,7 +415,7 @@ const char * const text_defaults[TOTAL_TEXT]={
 	,"\x01\x5f\x01\x79\x01\x68\x43\x72\x65\x64\x69\x74\x20\x76\x61\x6c\x75\x65\x20\x20\x20\x20\x20\x3a\x20\x01\x6e" // 257 EditCreditValue
 	,"\x01\x5f\x01\x79\x01\x68\x54\x69\x6d\x65\x73\x20\x64\x6f\x77\x6e\x6c\x6f\x61\x64\x65\x64\x20\x3a\x20\x01\x6e" // 258 EditTimesDownloaded
 	,"\x01\x5f\x01\x79\x01\x68\x4f\x70\x65\x6e\x20\x63\x6f\x75\x6e\x74\x20\x20\x20\x20\x20\x20\x20\x3a\x20\x01\x6e" // 259 EditOpenCount
-	,"\x01\x5f\x01\x79\x01\x68\x41\x6c\x74\x65\x72\x6e\x61\x74\x65\x20\x50\x61\x74\x68\x20\x20\x20\x3a\x20\x01\x6e" // 260 EditAltPath
+	,"\x55\x4e\x55\x53\x45\x44\x32\x36\x30" // 260 Unused260
 	,"\x0d\x0a\x01\x77\x01\x68\x59\x6f\x75\x20\x6f\x6e\x6c\x79\x20\x68\x61\x76\x65\x20\x25\x73\x20\x63\x72\x65\x64\x69\x74\x73\x2e\x0d"
 		"\x0a" // 261 YouOnlyHaveNCredits
 	,"\x0d\x0a\x59\x6f\x75\x20\x64\x6f\x6e\x27\x74\x20\x68\x61\x76\x65\x20\x65\x6e\x6f\x75\x67\x68\x20\x63\x72\x65\x64\x69\x74\x73\x2e"
@@ -428,7 +427,7 @@ const char * const text_defaults[TOTAL_TEXT]={
 	,"\x0d\x0a\x42\x75\x6c\x6b\x20\x55\x70\x6c\x6f\x61\x64\x20\x25\x73\x20\x25\x73\x20\x44\x69\x72\x65\x63\x74\x6f\x72\x79\x0d\x0a\x28"
 		"\x45\x6e\x74\x65\x72\x20\x27\x2d\x27\x20\x66\x6f\x72\x20\x64\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x20\x74\x6f\x20\x73\x6b\x69"
 		"\x70\x20\x66\x69\x6c\x65\x29\x3a\x0d\x0a" // 265 BulkUpload
-	,"\x01\x5f\x01\x79\x01\x68\x25\x73\x01\x77\x25\x37\x75\x6b\x01\x62\x3a" // 266 BulkUploadDescPrompt
+	,"\x01\x5f\x01\x79\x01\x68\x25\x2d\x31\x32\x73\x01\x77\x25\x37\x75\x6b\x01\x62\x3a" // 266 BulkUploadDescPrompt
 	,"\x0d\x0a\x01\x72\x01\x68\x01\x69\x4e\x6f\x20\x66\x69\x6c\x65\x73\x20\x69\x6e\x20\x62\x61\x74\x63\x68\x20\x71\x75\x65\x75\x65\x2e"
 		"\x01\x6e\x0d\x0a\x0d\x0a\x01\x6d\x55\x73\x65\x20\x01\x68\x44\x01\x6e\x01\x6d\x20\x6f\x72\x20\x01\x68\x55\x01\x6e\x01\x6d\x20\x74"
 		"\x6f\x20\x61\x64\x64\x20\x66\x69\x6c\x65\x73\x20\x74\x6f\x20\x74\x68\x65\x20\x71\x75\x65\x75\x65\x2e\x0d\x0a" // 267 NoFilesInBatchQueue
@@ -475,15 +474,13 @@ const char * const text_defaults[TOTAL_TEXT]={
 	,"\x0d\x0a\x55\x70\x6c\x6f\x61\x64\x65\x72\x3a\x20\x25\x73\x0d\x0a\x46\x69\x6c\x65\x6e\x61\x6d\x65\x3a\x20\x25\x73\x0d\x0a" // 294 TempFileInfo
 	,"\x0d\x0a\x25\x73\x20\x62\x79\x74\x65\x73\x20\x69\x6e\x20\x25\x75\x20\x66\x69\x6c\x65\x73\x0d\x0a" // 295 TempDirTotal
 	,"\x0d\x0a\x25\x75\x20\x66\x69\x6c\x65\x73\x20\x72\x65\x6d\x6f\x76\x65\x64\x2e\x0d\x0a" // 296 NFilesRemoved
-	,"\x01\x72\x01\x68\x01\x69\x41\x6c\x6c\x20\x6f\x74\x68\x65\x72\x20\x6e\x6f\x64\x65\x73\x20\x73\x68\x6f\x75\x6c\x64\x20\x4e\x4f\x54"
-		"\x20\x62\x65\x20\x69\x6e\x20\x75\x73\x65\x20\x64\x75\x72\x69\x6e\x67\x20\x72\x65\x73\x6f\x72\x74\x2f\x63\x6f\x6d\x70\x72\x65\x73"
-		"\x73\x69\x6f\x6e\x2e\x01\x6e\x0d\x0a" // 297 ResortWarning
-	,"\x01\x2d\x01\x63\x25\x2d\x31\x35\x2e\x31\x35\x73\x20\x01\x79\x01\x68\x25\x2d\x32\x35\x2e\x32\x35\x73\x20" // 298 ResortLineFmt
-	,"\x01\x62\x45\x6d\x70\x74\x79\x01\x6e\x0d\x0a" // 299 ResortEmptyDir
-	,"\x01\x77\x53\x6f\x72\x74\x69\x6e\x67\x20\x40\x45\x4c\x4c\x49\x50\x53\x49\x53\x40" // 300 Sorting
-	,"\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x01\x62\x53\x6f\x72\x74\x65\x64\x20\x20\x20\x20\x01\x6e\x0d\x0a" // 301 Sorted
-	,"\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x01\x62\x43\x6f\x6d\x70\x72\x65\x73\x73\x65\x64\x20\x25\x75\x20\x73\x6c\x6f\x74\x73\x20"
-		"\x28\x25\x73\x20\x62\x79\x74\x65\x73\x29\x01\x6e\x0d\x0a" // 302 Compressed
+	,"\x54\x61\x67\x20\x74\x68\x69\x73\x20\x66\x69\x6c\x65" // 297 TagFileQ
+	,"\x01\x68\x01\x79\x45\x6e\x74\x65\x72\x20\x28\x73\x70\x61\x63\x65\x2d\x73\x65\x70\x61\x72\x61\x74\x65\x64\x29\x20\x54\x61\x67\x73"
+		"\x3a\x20" // 298 TagFilePrompt
+	,"\x55\x4e\x55\x53\x45\x44\x32\x39\x39" // 299 Unused299
+	,"\x55\x4e\x55\x53\x45\x44\x33\x30\x30" // 300 Unused300
+	,"\x55\x4e\x55\x53\x45\x44\x33\x30\x31" // 301 Unused301
+	,"\x55\x4e\x55\x53\x45\x44\x33\x30\x32" // 302 Unused302
 	,"\x01\x77\x01\x68\x0d\x0a\x25\x73\x20\x69\x73\x20\x61\x6c\x72\x65\x61\x64\x79\x20\x69\x6e\x20\x74\x68\x65\x20\x71\x75\x65\x75\x65"
 		"\x2e\x0d\x0a" // 303 FileAlreadyInQueue
 	,"\x01\x77\x01\x68\x01\x2f\x46\x69\x6c\x65\x20\x69\x73\x20\x6e\x6f\x74\x20\x6f\x6e\x6c\x69\x6e\x65\x2e\x0d\x0a" // 304 FileIsNotOnline
@@ -506,7 +503,7 @@ const char * const text_defaults[TOTAL_TEXT]={
 		"\x0a\x01\x6e\x01\x67\x59\x6f\x75\x20\x77\x65\x72\x65\x20\x61\x77\x61\x72\x64\x65\x64\x20\x25\x73\x20\x63\x72\x65\x64\x69\x74\x73"
 		"\x2e\x0d\x0a" // 312 DownloadUserMsg
 	,"\x70\x61\x72\x74\x69\x61\x6c\x6c\x79\x20" // 313 Partially
-	,"\x0d\x0a\x01\x6e\x01\x67\x4c\x69\x62\x72\x61\x72\x79\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3a\x01\x68\x20\x28\x25\x75\x29\x20"
+	,"\x01\x6c\x01\x6e\x01\x67\x4c\x69\x62\x72\x61\x72\x79\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3a\x01\x68\x20\x28\x25\x75\x29\x20"
 		"\x25\x73" // 314 FiLib
 	,"\x0d\x0a\x01\x6e\x01\x67\x44\x69\x72\x65\x63\x74\x6f\x72\x79\x20\x20\x20\x20\x20\x20\x20\x20\x3a\x01\x68\x20\x28\x25\x75\x29\x20"
 		"\x25\x73" // 315 FiDir
@@ -521,11 +518,9 @@ const char * const text_defaults[TOTAL_TEXT]={
 	,"\x0d\x0a\x01\x6e\x01\x67\x4c\x61\x73\x74\x20\x64\x6f\x77\x6e\x6c\x6f\x61\x64\x65\x64\x20\x20\x3a\x01\x68\x20\x25\x73" // 323 FiDateDled
 	,"\x0d\x0a\x01\x6e\x01\x67\x54\x69\x6d\x65\x73\x20\x64\x6f\x77\x6e\x6c\x6f\x61\x64\x65\x64\x20\x3a\x01\x68\x20\x25\x75" // 324 FiTimesDled
 	,"\x0d\x0a\x01\x6e\x01\x67\x54\x69\x6d\x65\x20\x74\x6f\x20\x64\x6f\x77\x6e\x6c\x6f\x61\x64\x20\x3a\x01\x68\x20\x25\x73" // 325 FiTransferTime
-	,"\x0d\x0a\x01\x6e\x01\x67\x41\x6c\x74\x65\x72\x6e\x61\x74\x65\x20\x50\x61\x74\x68\x20\x20\x20\x3a\x01\x68\x20\x25\x73" // 326 FiAlternatePath
-	,"\x0d\x0a\x01\x72\x01\x68\x01\x69\x49\x6e\x76\x61\x6c\x69\x64\x20\x41\x6c\x74\x65\x72\x6e\x61\x74\x65\x20\x50\x61\x74\x68\x20\x4e"
-		"\x75\x6d\x62\x65\x72\x3a\x20\x25\x75\x01\x6e" // 327 InvalidAlternatePathN
-	,"\x01\x5f\x01\x2f\x01\x77\x01\x68\x46\x69\x6c\x65\x20\x69\x73\x20\x63\x75\x72\x72\x65\x6e\x74\x6c\x79\x20\x6f\x70\x65\x6e\x20\x62"
-		"\x79\x20\x25\x64\x20\x75\x73\x65\x72\x25\x73\x2e\x0d\x0a" // 328 FileIsOpen
+	,"\x0d\x0a\x01\x6e\x01\x67\x54\x61\x67\x73\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3a\x01\x68\x20\x25\x73" // 326 FiTags
+	,"\x55\x4e\x55\x53\x45\x44\x33\x32\x37" // 327 Unused327
+	,"\x0d\x0a\x01\x6e\x01\x67\x46\x69\x6c\x65\x20\x25\x2d\x36\x2e\x36\x73\x20\x20\x20\x20\x20\x20\x3a\x01\x68\x20\x25\x73" // 328 FiChecksum
 	,"\x07\x07\x0d\x0a\x01\x68\x01\x72\x48\x01\x62\x61\x01\x67\x70\x01\x79\x70\x01\x63\x79\x20\x01\x6d\x42\x01\x77\x69\x01\x72\x72\x01"
 		"\x67\x74\x01\x62\x68\x01\x63\x64\x01\x6d\x61\x01\x79\x79\x20\x01\x77\x74\x01\x72\x6f\x20\x01\x67\x79\x01\x62\x6f\x01\x63\x75\x0d"
 		"\x0a\x07\x07\x01\x6d\x48\x01\x79\x61\x01\x77\x70\x01\x72\x70\x01\x67\x79\x20\x01\x62\x42\x01\x63\x69\x01\x6d\x72\x01\x79\x74\x01"
@@ -586,10 +581,8 @@ const char * const text_defaults[TOTAL_TEXT]={
 	,"\x07\x01\x72\x01\x68\x01\x69\x25\x64\x20\x63\x72\x69\x74\x69\x63\x61\x6c\x20\x65\x72\x72\x6f\x72\x73\x20\x68\x61\x76\x65\x20\x6f"
 		"\x63\x63\x75\x72\x72\x65\x64\x2e\x20\x54\x79\x70\x65\x20\x3b\x45\x52\x52\x20\x61\x74\x20\x6d\x61\x69\x6e\x20\x6d\x65\x6e\x75\x2e"
 		"\x01\x6e\x0d\x0a" // 359 CriticalErrors
-	,"\x01\x5f\x01\x77\x01\x68\x59\x6f\x75\x20\x68\x61\x76\x65\x20\x25\x64\x20\x55\x73\x65\x72\x20\x74\x6f\x20\x55\x73\x65\x72\x20\x54"
-		"\x72\x61\x6e\x73\x66\x65\x72\x25\x73\x20\x77\x61\x69\x74\x69\x6e\x67\x20\x66\x6f\x72\x20\x79\x6f\x75\x0d\x0a" // 360 UserXferForYou
-	,"\x01\x5f\x01\x77\x01\x68\x59\x6f\x75\x20\x68\x61\x76\x65\x20\x73\x65\x6e\x74\x20\x25\x64\x20\x75\x6e\x72\x65\x63\x65\x69\x76\x65"
-		"\x64\x20\x55\x73\x65\x72\x20\x74\x6f\x20\x55\x73\x65\x72\x20\x54\x72\x61\x6e\x73\x66\x65\x72\x25\x73\x0d\x0a" // 361 UnreceivedUserXfer
+	,"\x55\x6e\x75\x73\x65\x64\x33\x36\x30" // 360 UserXferForYou
+	,"\x55\x6e\x75\x73\x65\x64\x33\x36\x31" // 361 UnreceivedUserXfer
 	,"\x52\x65\x61\x64\x20\x79\x6f\x75\x72\x20\x6d\x61\x69\x6c\x20\x6e\x6f\x77" // 362 ReadYourMailNowQ
 	,"\x53\x6f\x72\x72\x79\x2c\x20\x74\x68\x65\x20\x73\x79\x73\x74\x65\x6d\x20\x69\x73\x20\x63\x6c\x6f\x73\x65\x64\x20\x74\x6f\x20\x6e"
 		"\x65\x77\x20\x75\x73\x65\x72\x73\x2e\x0d\x0a" // 363 NoNewUsers
@@ -991,8 +984,7 @@ const char * const text_defaults[TOTAL_TEXT]={
 		"\x79\x6f\x75\x72\x20\x61\x63\x63\x6f\x75\x6e\x74\x2e\x0d\x0a" // 592 CreditedAccount
 	,"\x0d\x0a\x41\x4e\x53\x49\x20\x43\x61\x70\x74\x75\x72\x65\x20\x69\x73\x20\x6e\x6f\x77\x20\x25\x73\x0d\x0a" // 593 ANSICaptureIsNow
 	,"\x01\x6e\x01\x6d\x0d\x0a\x52\x65\x74\x72\x69\x65\x76\x69\x6e\x67\x20\x01\x68\x25\x73\x01\x6e\x01\x6d\x2e\x2e\x2e" // 594 RetrievingFile
-	,"\x01\x6e\x0d\x0a\x41\x6c\x74\x65\x72\x6e\x61\x74\x65\x20\x75\x70\x6c\x6f\x61\x64\x20\x70\x61\x74\x68\x20\x6e\x6f\x77\x3a\x20\x25"
-		"\x73\x0d\x0a" // 595 AltULPathIsNow
+	,"\x55\x4e\x55\x53\x45\x44\x35\x39\x35" // 595 Unused595
 	,"\x0d\x0a\x50\x72\x69\x76\x61\x74\x65" // 596 PrivatePostQ
 	,"\x0d\x0a\x01\x5f\x01\x79\x01\x68\x50\x6f\x73\x74\x20\x74\x6f\x3a\x20" // 597 PostTo
 	,"\x0d\x0a\x50\x72\x69\x76\x61\x74\x65\x20\x70\x6f\x73\x74\x73\x20\x72\x65\x71\x75\x69\x72\x65\x20\x61\x20\x64\x65\x73\x74\x69\x6e"
diff --git a/src/sbbs3/tmp_xfer.cpp b/src/sbbs3/tmp_xfer.cpp
index 4969680f0c26c77a9b8164557e0baee597c13928..e6cbbd0644d4a85d77ea1c7550b1c96f5ac25741 100644
--- a/src/sbbs3/tmp_xfer.cpp
+++ b/src/sbbs3/tmp_xfer.cpp
@@ -1,9 +1,4 @@
-/* tmp_xfer.cpp */
-
 /* Synchronet temp directory file transfer routines */
-// vi: tabstop=4
-
-/* $Id: tmp_xfer.cpp,v 1.51 2020/05/14 07:50:00 rswindell Exp $ */
 
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
@@ -18,21 +13,9 @@
  * See the GNU General Public License for more details: gpl.txt or			*
  * http://www.fsf.org/copyleft/gpl.html										*
  *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
- *																			*
  * For Synchronet coding style and modification guidelines, see				*
  * http://www.synchro.net/source.html										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
@@ -44,6 +27,7 @@
 /*****************************************************************************/
 void sbbs_t::temp_xfer()
 {
+#if 0	// TODO
     char	str[256],tmp2[256],done=0,ch;
 	char 	tmp[512];
 	int		error;
@@ -195,7 +179,7 @@ void sbbs_t::temp_xfer()
 					if(cfg.dir[temp_dirnum]->misc&DIR_TFREE)
 						starttime+=end-start;
 					if(checkprotresult(cfg.prot[i],error,&f))
-						downloadfile(&f);
+						downloadedfile(&f);
 					else
 						notdownloaded(f.size,start,end);
 					autohangup(); 
@@ -279,6 +263,7 @@ void sbbs_t::temp_xfer()
 	}
 	free(cfg.dir[dirnum]);
 	cfg.total_dirs--;
+#endif
 }
 
 /*****************************************************************************/
@@ -286,6 +271,7 @@ void sbbs_t::temp_xfer()
 /*****************************************************************************/
 void sbbs_t::extract(uint dirnum)
 {
+#if 0 // NFB-TODO
     char	fname[13],str[256],excmd[256],path[256],done
 				,tmp[256],intmp=0;
     uint	i,j;
@@ -348,11 +334,11 @@ void sbbs_t::extract(uint dirnum)
 		bputs(text[UnextractableFile]);
 		return; 
 	}
-	if(!intmp && !findfile(&cfg,dirnum,f.name)) {    /* not temp dir */
+	if(!intmp && !findfile(&cfg, dirnum, f.name, NULL)) {    /* not temp dir */
 		bputs(text[SearchingAllDirs]);
 		for(i=0;i<usrdirs[curlib] && !msgabort();i++) {
 			if(i==dirnum) continue;
-			if(findfile(&cfg,usrdir[curlib][i],f.name))
+			if(findfile(&cfg, usrdir[curlib][i], f.name, NULL))
 				break; 
 		}
 		if(i==usrdirs[curlib]) { /* not found in cur lib */
@@ -360,7 +346,7 @@ void sbbs_t::extract(uint dirnum)
 			for(i=j=0;i<usrlibs;i++) {
 				if(i==curlib) continue;
 				for(j=0;j<usrdirs[i] && !msgabort();j++)
-					if(findfile(&cfg,usrdir[i][j],f.name))
+					if(findfile(&cfg, usrdir[i][j], f.name, NULL))
 						break;
 				if(j<usrdirs[i])
 					break; 
@@ -425,6 +411,7 @@ void sbbs_t::extract(uint dirnum)
 				break; 
 		} 
 	}
+#endif
 }
 
 /****************************************************************************/
@@ -434,21 +421,20 @@ void sbbs_t::extract(uint dirnum)
 ulong sbbs_t::create_filelist(const char *name, long mode)
 {
     char	str[256];
-	int		file;
+	FILE*	fp;
 	uint	i,j,d;
 	ulong	l,k;
 
 	if(online == ON_REMOTE)
 		bprintf(text[CreatingFileList],name);
 	SAFEPRINTF2(str,"%s%s",cfg.temp_dir,name);
-	if((file=nopen(str,O_CREAT|O_WRONLY|O_APPEND))==-1) {
+	if((fp = fopen(str,"ab")) == NULL) {
 		errormsg(WHERE,ERR_OPEN,str,O_CREAT|O_WRONLY|O_APPEND);
 		return(0);
 	}
 	k=0;
 	if(mode&FL_ULTIME) {
-		SAFEPRINTF(str,"New files since: %s\r\n",timestr(ns_time));
-		write(file,str,strlen(str));
+		fprintf(fp, "New files since: %s\r\n", timestr(ns_time));
 	}
 	for(i=j=d=0;i<usrlibs;i++) {
 		for(j=0;j<usrdirs[i];j++,d++) {
@@ -459,7 +445,7 @@ ulong sbbs_t::create_filelist(const char *name, long mode)
 				&& (cfg.lib[usrlib[i]]->offline_dir==usrdir[i][j]
 				|| cfg.dir[usrdir[i][j]]->misc&DIR_NOSCAN))
 				continue;
-			l=listfiles(usrdir[i][j],nulstr,file,mode);
+			l=listfiles(usrdir[i][j], nulstr, fp, mode);
 			if((long)l==-1)
 				break;
 			k+=l;
@@ -468,10 +454,9 @@ ulong sbbs_t::create_filelist(const char *name, long mode)
 			break;
 	}
 	if(k>1) {
-		SAFEPRINTF(str,"\r\n%ld Files Listed.\r\n",k);
-		write(file,str,strlen(str));
+		fprintf(fp,"\r\n%ld Files Listed.\r\n",k);
 	}
-	close(file);
+	fclose(fp);
 	if(k)
 		bprintf(text[CreatedFileList],name);
 	else {
@@ -489,7 +474,7 @@ ulong sbbs_t::create_filelist(const char *name, long mode)
 /* This function returns the command line for the temp file extension for	*/
 /* current user online. 													*/
 /****************************************************************************/
-char * sbbs_t::temp_cmd(void)
+const char* sbbs_t::temp_cmd(void)
 {
 	int i;
 
diff --git a/src/sbbs3/uedit/uedit.c b/src/sbbs3/uedit/uedit.c
index f6401ba2a307a538da897474788ac26d03d2bed2..293ea6d6f1b5e8714099517cf6e6a38857f03106 100644
--- a/src/sbbs3/uedit/uedit.c
+++ b/src/sbbs3/uedit/uedit.c
@@ -538,7 +538,7 @@ int edit_xedit(scfg_t *cfg, user_t *user)
 				if(j > 0)
 				    putuserrec(cfg,user->number,U_XEDIT,8,cfg->xedit[j-1]->code);
 				else
-				    putuserrec(cfg,user->number,U_XEDIT,8,nulstr);
+				    putuserrec(cfg,user->number,U_XEDIT,8,"");
 			}
 			break;
 	}
@@ -1632,7 +1632,7 @@ int edit_user(scfg_t *cfg, int usernum)
 				user.misc ^= DELETED;
 				putuserrec(cfg,user.number,U_MISC,8,ultoa(user.misc,str,16));
 				if(user.misc & DELETED)
-					putusername(cfg,user.number,nulstr);
+					putusername(cfg,user.number,"");
 				else
 					putusername(cfg,user.number,user.alias);
 				break;
diff --git a/src/sbbs3/un_qwk.cpp b/src/sbbs3/un_qwk.cpp
index 0d35a98cc6325465695361370cbc5eab6d366745..ac05baf2f7003144c0e4db524a0d2573e2d6499c 100644
--- a/src/sbbs3/un_qwk.cpp
+++ b/src/sbbs3/un_qwk.cpp
@@ -21,6 +21,7 @@
 
 #include "sbbs.h"
 #include "qwk.h"
+#include "filedat.h"
 
 static void log_qwk_import_stats(sbbs_t* sbbs, ulong msgs, time_t start)
 {
@@ -39,6 +40,7 @@ bool sbbs_t::unpack_qwk(char *packet,uint hubnum)
 {
 	char	str[MAX_PATH+1],fname[MAX_PATH+1];
 	char 	tmp[512];
+	char	error[256] = "";
 	char	inbox[MAX_PATH+1];
 	uchar	block[QWK_BLOCK_LEN];
 	int 	k,file;
@@ -74,10 +76,24 @@ bool sbbs_t::unpack_qwk(char *packet,uint hubnum)
 		return(false);
 	}
 	delfiles(cfg.temp_dir,ALLFILES);
-	i=external(cmdstr(cfg.qhub[hubnum]->unpack,packet,ALLFILES,NULL),EX_OFFLINE);
-	if(i) {
-		errormsg(WHERE,ERR_EXEC,cmdstr(cfg.qhub[hubnum]->unpack,packet,ALLFILES,NULL),i);
-		return(false); 
+	long file_count = extract_files_from_archive(packet
+		,/* outdir: */cfg.temp_dir
+		,/* allowed_filename_chars: */NULL /* any */
+		,/* with_path: */false
+		,/* max_files: */0 /* unlimited */
+		,/* file_list: */NULL /* all files */
+		,error, sizeof(error));
+	if(file_count >= 0) {
+		lprintf(LOG_DEBUG, "libarchive extracted %ld files from %s", file_count, packet);
+	} else {
+		lprintf(LOG_ERR, "libarchive error %ld (%s) extracting %s", file_count, error, packet);
+		if(*cfg.qhub[hubnum]->unpack == '\0')
+			return false;
+		i=external(cmdstr(cfg.qhub[hubnum]->unpack,packet,ALLFILES,NULL),EX_OFFLINE);
+		if(i) {
+			errormsg(WHERE,ERR_EXEC,cmdstr(cfg.qhub[hubnum]->unpack,packet,ALLFILES,NULL),i);
+			return(false); 
+		}
 	}
 	SAFEPRINTF(str,"%sMESSAGES.DAT",cfg.temp_dir);
 	if(!fexistcase(str)) {
diff --git a/src/sbbs3/un_rep.cpp b/src/sbbs3/un_rep.cpp
index 6e77cdc4da5efb533aa866bd53d6d74889108c02..26eb2c4026ad5ea2c6a0c5c4841856411eca1432 100644
--- a/src/sbbs3/un_rep.cpp
+++ b/src/sbbs3/un_rep.cpp
@@ -21,6 +21,7 @@
 
 #include "sbbs.h"
 #include "qwk.h"
+#include "filedat.h"
 
 /****************************************************************************/
 /* Unpacks .REP packet, 'repfile' is the path and filename of the packet    */
@@ -31,6 +32,7 @@ bool sbbs_t::unpack_rep(char* repfile)
 	char	rep_fname[MAX_PATH+1];
 	char	msg_fname[MAX_PATH+1];
 	char 	tmp[512];
+	char	error[256];
 	char	inbox[MAX_PATH+1];
 	char	block[QWK_BLOCK_LEN];
 	int 	file;
@@ -70,20 +72,33 @@ bool sbbs_t::unpack_rep(char* repfile)
 		logline(LOG_NOTICE,nulstr,"REP file not received");
 		return(false); 
 	}
-	for(k=0;k<cfg.total_fextrs;k++)
-		if(!stricmp(cfg.fextr[k]->ext,useron.tmpext) && chk_ar(cfg.fextr[k]->ar,&useron,&client))
-			break;
-	if(k>=cfg.total_fextrs)
-		k=0;
-	ex=EX_STDOUT;
-	if(online!=ON_REMOTE)
-		ex|=EX_OFFLINE;
-	i=external(cmdstr(cfg.fextr[k]->cmd,rep_fname,ALLFILES,NULL),ex);
-	if(i) {
-		bputs(text[QWKExtractionFailed]);
-		logline(LOG_NOTICE,"U!",AttemptedToUploadREPpacket);
-		logline(LOG_NOTICE,nulstr,"Extraction failed");
-		return(false); 
+	long file_count = extract_files_from_archive(rep_fname
+		,/* outdir: */cfg.temp_dir
+		,/* allowed_filename_chars: */SAFEST_FILENAME_CHARS
+		,/* with_path: */false
+		,/* max_files */1000
+		,/* file_list: */NULL /* all files */
+		,error, sizeof(error));
+	if(file_count > 0) {
+		lprintf(LOG_DEBUG, "libarchive extracted %lu files from %s", file_count, rep_fname);
+	} else {
+		if(*error)
+			lprintf(LOG_NOTICE, "libarchive error (%s) extracting %s", error, rep_fname);
+		for(k=0;k<cfg.total_fextrs;k++)
+			if(!stricmp(cfg.fextr[k]->ext,useron.tmpext) && chk_ar(cfg.fextr[k]->ar,&useron,&client))
+				break;
+		if(k>=cfg.total_fextrs)
+			k=0;
+		ex=EX_STDOUT;
+		if(online!=ON_REMOTE)
+			ex|=EX_OFFLINE;
+		i=external(cmdstr(cfg.fextr[k]->cmd,rep_fname,ALLFILES,NULL),ex);
+		if(i) {
+			bputs(text[QWKExtractionFailed]);
+			logline(LOG_NOTICE,"U!",AttemptedToUploadREPpacket);
+			logline(LOG_NOTICE,nulstr,"Extraction failed");
+			return(false); 
+		}
 	}
 	SAFEPRINTF2(msg_fname,"%s%s.msg",cfg.temp_dir,cfg.sys_id);
 	if(!fexistcase(msg_fname)) {
diff --git a/src/sbbs3/unbaja.c b/src/sbbs3/unbaja.c
index 91e4ab271c8c6ff204119162d0a775a41628b219..6da7e1af049dd2312cd78e1c1482c936dfe53016 100644
--- a/src/sbbs3/unbaja.c
+++ b/src/sbbs3/unbaja.c
@@ -1330,10 +1330,10 @@ void decompile(FILE *bin, FILE *srcfile)
 	char	src[2048];
 	int		redo=FALSE;
 	char	*labels;
-	size_t	currpos=0;
+	off_t	currpos=0;
 
 	currpos=filelength(fileno(bin));
-	labels=(char *)calloc(1,filelength(fileno(bin)));
+	labels=(char *)calloc(1,(size_t)filelength(fileno(bin)));
 	if(labels==NULL) {
 		printf("ERROR allocating memory for labels\n");
 		return;
@@ -1351,7 +1351,7 @@ void decompile(FILE *bin, FILE *srcfile)
 		}
 		src[0]=0;
 		if(labels[currpos])
-			sprintf(src,":label_%04" XP_PRIsize_t "x\n",currpos);
+			sprintf(src,":label_%04" XP_PRIsize_t "x\n", (size_t)currpos);
 		switch(uch) {
 			case CS_USE_INT_VAR:
 				usevar=TRUE;
diff --git a/src/sbbs3/upgrade_to_v319.c b/src/sbbs3/upgrade_to_v319.c
new file mode 100644
index 0000000000000000000000000000000000000000..5bd114c4df8d791a5be549d025e72411221f9dcc
--- /dev/null
+++ b/src/sbbs3/upgrade_to_v319.c
@@ -0,0 +1,717 @@
+/* Upgrade Synchronet files from v3.18 to v3.19 */
+
+/****************************************************************************
+ * @format.tab-size 4		(Plain Text/Source Code File Header)			*
+ * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
+ *																			*
+ * Copyright Rob Swindell - http://www.synchro.net/copyright.html			*
+ *																			*
+ * 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.					*
+ * See the GNU General Public License for more details: gpl.txt or			*
+ * http://www.fsf.org/copyleft/gpl.html										*
+ *																			*
+ * For Synchronet coding style and modification guidelines, see				*
+ * http://www.synchro.net/source.html										*
+ *																			*
+ * Note: If this box doesn't appear square, then you need to fix your tabs.	*
+ ****************************************************************************/
+
+#include <stdbool.h>
+#include "sbbs.h"
+#include "sbbs4defs.h"
+#include "ini_file.h"
+#include "dat_file.h"
+#include "datewrap.h"
+#include "filedat.h"
+
+scfg_t scfg;
+BOOL overwrite_existing_files=TRUE;
+ini_style_t style = { 25, NULL, NULL, " = ", NULL };
+
+BOOL overwrite(const char* path)
+{
+	char	str[128];
+
+	if(!overwrite_existing_files && fexist(path)) {
+		printf("\n%s already exists, overwrite? ",path);
+		fgets(str,sizeof(str),stdin);
+		if(toupper(*str)!='Y')
+			return(FALSE);
+	}
+
+	return(TRUE);
+}
+
+int lprintf(int level, const char *fmt, ...)
+{
+	va_list argptr;
+	char sbuf[1024];
+
+    va_start(argptr,fmt);
+    vsnprintf(sbuf,sizeof(sbuf),fmt,argptr);
+	sbuf[sizeof(sbuf)-1]=0;
+    va_end(argptr);
+    return(puts(sbuf));
+}
+
+/****************************************************************************/
+/* Converts a date string in format MM/DD/YY into unix time format			*/
+/****************************************************************************/
+long dstrtodate(scfg_t* cfg, char *instr)
+{
+	char*	p;
+	char*	day;
+	char	str[16];
+	struct tm tm;
+
+	if(!instr[0] || !strncmp(instr,"00/00/00",8))
+		return(0);
+
+	if(isdigit(instr[0]) && isdigit(instr[1])
+		&& isdigit(instr[3]) && isdigit(instr[4])
+		&& isdigit(instr[6]) && isdigit(instr[7]))
+		p=instr;	/* correctly formatted */
+	else {
+		p=instr;	/* incorrectly formatted */
+		while(*p && isdigit(*p)) p++;
+		if(*p==0)
+			return(0);
+		p++;
+		day=p;
+		while(*p && isdigit(*p)) p++;
+		if(*p==0)
+			return(0);
+		p++;
+		sprintf(str,"%02u/%02u/%02u"
+			,atoi(instr)%100,atoi(day)%100,atoi(p)%100);
+		p=str;
+	}
+
+	memset(&tm,0,sizeof(tm));
+	tm.tm_year=((p[6]&0xf)*10)+(p[7]&0xf);
+	if(cfg->sys_misc&SM_EURODATE) {
+		tm.tm_mon=((p[3]&0xf)*10)+(p[4]&0xf);
+		tm.tm_mday=((p[0]&0xf)*10)+(p[1]&0xf); }
+	else {
+		tm.tm_mon=((p[0]&0xf)*10)+(p[1]&0xf);
+		tm.tm_mday=((p[3]&0xf)*10)+(p[4]&0xf); }
+
+	return(((tm.tm_year+1900)*10000)+(tm.tm_mon*100)+tm.tm_mday);
+}
+
+/*****************************/
+/* LEGACY FILEBASE CONSTANTS */
+/*****************************/
+#define LEN_FCDT		 9	/* 9 digits for file credit values				*/
+/****************************************************************************/
+/* Offsets into DIR .DAT file for different fields for each file 			*/
+/****************************************************************************/
+#define F_CDT		0				/* Offset in DIR#.DAT file for cdts		*/
+#define F_DESC		(F_CDT+LEN_FCDT)/* Description							*/
+#define F_ULER		(F_DESC+LEN_FDESC+2)   /* Uploader						*/
+#define F_TIMESDLED (F_ULER+30+2) 	/* Number of times downloaded 			*/
+#define F_OPENCOUNT	(F_TIMESDLED+5+2)
+#define F_MISC		(F_OPENCOUNT+3+2)
+#define F_ALTPATH	(F_MISC+1)		/* Two hex digit alternate path */
+#define F_LEN		(F_ALTPATH+2+2) /* Total length of all fdat in file		*/
+
+#define F_IXBSIZE	22				/* Length of each index entry			*/
+
+#define F_EXBSIZE	512				/* Length of each ext-desc entry		*/
+
+/***********************/
+/* LEGACY FILEBASE API */
+/***********************/
+typedef struct {						/* File (transfers) Data */
+	char    name[13],					/* Name of file FILENAME.EXT */
+			desc[LEN_FDESC+1],			/* Uploader's Description */
+			uler[LEN_ALIAS+1];			/* User who uploaded */
+	uchar	opencount;					/* Times record is currently open */
+	time32_t  date,						/* File date/time */
+			dateuled,					/* Date/Time (Unix) Uploaded */
+			datedled;					/* Date/Time (Unix) Last downloaded */
+	uint16_t	dir,						/* Directory file is in */
+			altpath,
+			timesdled,					/* Total times downloaded */
+			timetodl;					/* How long transfer time */
+	int32_t	datoffset,					/* Offset into .DAT file */
+			size,						/* Size of file */
+			misc;						/* Miscellaneous bits */
+	uint32_t	cdt;						/* Credit value for this file */
+
+} oldfile_t;
+
+                                    /* Bit values for file_t.misc */
+#define FM_EXTDESC  (1<<0)          /* Extended description exists */
+#define FM_ANON 	(1<<1)			/* Anonymous upload */
+
+/****************************************************************************/
+/* Turns FILE.EXT into FILE    .EXT                                         */
+/****************************************************************************/
+char* padfname(const char *filename, char *str)
+{
+    int c,d;
+
+	for(c=0;c<8;c++)
+		if(filename[c]=='.' || !filename[c]) break;
+		else str[c]=filename[c];
+	d=c;
+	if(filename[c]=='.') c++;
+	while(d<8)
+		str[d++]=' ';
+	if(filename[c]>' ')	/* Change "FILE" to "FILE        " */
+		str[d++]='.';	/* (don't add a dot if there's no extension) */
+	else
+		str[d++]=' ';
+	while(d<12)
+		if(!filename[c]) break;
+		else str[d++]=filename[c++];
+	while(d<12)
+		str[d++]=' ';
+	str[d]=0;
+	return(str);
+}
+
+/****************************************************************************/
+/* Turns FILE    .EXT into FILE.EXT                                         */
+/****************************************************************************/
+char* unpadfname(const char *filename, char *str)
+{
+    int c,d;
+
+	for(c=0,d=0;filename[c];c++)
+		if(filename[c]!=' ') str[d++]=filename[c];
+	str[d]=0;
+	return(str);
+}
+
+/****************************************************************************/
+/* Returns full (case-corrected) path to specified file						*/
+/****************************************************************************/
+char* getoldfilepath(scfg_t* cfg, oldfile_t* f, char* path)
+{
+	char	fname[MAX_PATH+1];
+
+	unpadfname(f->name,fname);
+	if(f->dir>=cfg->total_dirs)
+		safe_snprintf(path,MAX_PATH,"%s%s",cfg->temp_dir,fname);
+	else
+		safe_snprintf(path,MAX_PATH,"%s%s",f->altpath>0 && f->altpath<=cfg->altpaths 
+			? cfg->altpath[f->altpath-1] : cfg->dir[f->dir]->path
+			,fname);
+	if(!fexistcase(path)) {
+		char tmp[MAX_PATH + 1];
+		safe_snprintf(tmp,MAX_PATH,"%s%s",f->altpath>0 && f->altpath<=cfg->altpaths 
+			? cfg->altpath[f->altpath-1] : cfg->dir[f->dir]->path
+			,f->desc);
+		if(fexistcase(tmp))
+			strcpy(path, tmp);
+	}
+	return(path);
+}
+
+int file_uldate_compare(const void* v1, const void* v2)
+{
+	oldfile_t* f1 = (oldfile_t*)v1;
+	oldfile_t* f2 = (oldfile_t*)v2;
+
+	return f1->dateuled - f2->dateuled;
+}
+
+/****************************************************************************/
+/* Gets filedata from dircode.DAT file										*/
+/* Need fields .name ,.dir and .offset to get other info    				*/
+/* Does not fill .dateuled or .datedled fields.                             */
+/****************************************************************************/
+BOOL getfiledat(scfg_t* cfg, oldfile_t* f)
+{
+	char buf[F_LEN+1],str[MAX_PATH+1];
+	int file;
+	long length;
+
+	SAFEPRINTF2(str,"%s%s.dat",cfg->dir[f->dir]->data_dir,cfg->dir[f->dir]->code);
+	if((file=sopen(str,O_RDONLY|O_BINARY,SH_DENYWR))==-1) {
+		return(FALSE); 
+	}
+	length=(long)filelength(file);
+	if(f->datoffset>length) {
+		close(file);
+		return(FALSE); 
+	}
+	if(length%F_LEN) {
+		close(file);
+		return(FALSE); 
+	}
+	lseek(file,f->datoffset,SEEK_SET);
+	if(read(file,buf,F_LEN)!=F_LEN) {
+		close(file);
+		return(FALSE); 
+	}
+	close(file);
+	getrec(buf,F_ALTPATH,2,str);
+	f->altpath=hptoi(str);
+	getrec(buf,F_CDT,LEN_FCDT,str);
+	f->cdt=atol(str);
+
+	if(f->size == 0) {					// only read disk if f->size == 0
+		struct stat st;
+		getoldfilepath(cfg,f,str);
+		if(stat(str, &st) == 0) {
+			f->size = (int32_t)st.st_size;
+			f->date = (time32_t)st.st_mtime;
+		} else
+			f->size = -1;	// indicates file does not exist
+	}
+#if 0
+	if((f->size>0L) && cur_cps)
+		f->timetodl=(ushort)(f->size/(ulong)cur_cps);
+	else
+#endif
+		f->timetodl=0;
+
+	getrec(buf,F_DESC,LEN_FDESC,f->desc);
+	getrec(buf,F_ULER,LEN_ALIAS,f->uler);
+	getrec(buf,F_TIMESDLED,5,str);
+	f->timesdled=atoi(str);
+	getrec(buf,F_OPENCOUNT,3,str);
+	f->opencount=atoi(str);
+	if(buf[F_MISC]!=ETX)
+		f->misc=buf[F_MISC]-' ';
+	else
+		f->misc=0;
+	return(TRUE);
+}
+
+/****************************************************************************/
+/* Puts filedata into DIR_code.DAT file                                     */
+/* Called from removefiles                                                  */
+/****************************************************************************/
+BOOL putfiledat(scfg_t* cfg, oldfile_t* f)
+{
+    char buf[F_LEN+1],str[MAX_PATH+1],tmp[128];
+    int file;
+    long length;
+
+	putrec(buf,F_CDT,LEN_FCDT,ultoa(f->cdt,tmp,10));
+	putrec(buf,F_DESC,LEN_FDESC,f->desc);
+	putrec(buf,F_DESC+LEN_FDESC,2, "\r\n");
+	putrec(buf,F_ULER,LEN_ALIAS+5,f->uler);
+	putrec(buf,F_ULER+LEN_ALIAS+5,2, "\r\n");
+	putrec(buf,F_TIMESDLED,5,ultoa(f->timesdled,tmp,10));
+	putrec(buf,F_TIMESDLED+5,2, "\r\n");
+	putrec(buf,F_OPENCOUNT,3,ultoa(f->opencount,tmp,10));
+	putrec(buf,F_OPENCOUNT+3,2, "\r\n");
+	buf[F_MISC]=(char)f->misc+' ';
+	putrec(buf,F_ALTPATH,2,hexplus(f->altpath,tmp));
+	putrec(buf,F_ALTPATH+2,2, "\r\n");
+	SAFEPRINTF2(str,"%s%s.dat",cfg->dir[f->dir]->data_dir,cfg->dir[f->dir]->code);
+	if((file=sopen(str,O_WRONLY|O_BINARY,SH_DENYRW))==-1) {
+		return(FALSE); 
+	}
+	length=(long)filelength(file);
+	if(length%F_LEN) {
+		close(file);
+		return(FALSE); 
+	}
+	if(f->datoffset>length) {
+		close(file);
+		return(FALSE); 
+	}
+	lseek(file,f->datoffset,SEEK_SET);
+	if(write(file,buf,F_LEN)!=F_LEN) {
+		close(file);
+		return(FALSE); 
+	}
+	length=(long)filelength(file);
+	close(file);
+	if(length%F_LEN) {
+		return(FALSE);
+	}
+	return(TRUE);
+}
+
+/****************************************************************************/
+/* Gets file data from dircode.ixb file										*/
+/* Need fields .name and .dir filled.                                       */
+/* only fills .offset, .dateuled, and .datedled                             */
+/****************************************************************************/
+BOOL getfileixb(scfg_t* cfg, oldfile_t* f)
+{
+	char			str[MAX_PATH+1],fname[13];
+	uchar *	ixbbuf;
+	int				file;
+	long			l,length;
+
+	SAFEPRINTF2(str,"%s%s.ixb",cfg->dir[f->dir]->data_dir,cfg->dir[f->dir]->code);
+	if((file=sopen(str,O_RDONLY|O_BINARY,SH_DENYWR))==-1) {
+		return(FALSE); 
+	}
+	length=(long)filelength(file);
+	if(length%F_IXBSIZE) {
+		close(file);
+		return(FALSE); 
+	}
+	if((ixbbuf=(uchar *)malloc(length))==NULL) {
+		close(file);
+		return(FALSE); 
+	}
+	if(read(file,ixbbuf,length)!=length) {
+		close(file);
+		free(ixbbuf);
+		return(FALSE); 
+	}
+	close(file);
+	SAFECOPY(fname,f->name);
+	for(l=8;l<12;l++)	/* Turn FILENAME.EXT into FILENAMEEXT */
+		fname[l]=fname[l+1];
+	for(l=0;l<length;l+=F_IXBSIZE) {
+		SAFEPRINTF(str,"%11.11s",ixbbuf+l);
+		if(!stricmp(str,fname))
+			break; 
+	}
+	if(l>=length) {
+		free(ixbbuf);
+		return(FALSE); 
+	}
+	l+=11;
+	f->datoffset=ixbbuf[l]|((long)ixbbuf[l+1]<<8)|((long)ixbbuf[l+2]<<16);
+	f->dateuled=ixbbuf[l+3]|((long)ixbbuf[l+4]<<8)
+		|((long)ixbbuf[l+5]<<16)|((long)ixbbuf[l+6]<<24);
+	f->datedled=ixbbuf[l+7]|((long)ixbbuf[l+8]<<8)
+		|((long)ixbbuf[l+9]<<16)|((long)ixbbuf[l+10]<<24);
+	free(ixbbuf);
+	return(TRUE);
+}
+
+/****************************************************************************/
+/* Updates the datedled and dateuled index record fields for a file			*/
+/****************************************************************************/
+BOOL putfileixb(scfg_t* cfg, oldfile_t* f)
+{
+	char	str[MAX_PATH+1],fname[13];
+	uchar*	ixbbuf;
+	int		file;
+	long	l,length;
+
+	SAFEPRINTF2(str,"%s%s.ixb",cfg->dir[f->dir]->data_dir,cfg->dir[f->dir]->code);
+	if((file=sopen(str,O_RDWR|O_BINARY,SH_DENYRW))==-1) {
+		return(FALSE); 
+	}
+	length=(long)filelength(file);
+	if(length%F_IXBSIZE) {
+		close(file);
+		return(FALSE); 
+	}
+	if((ixbbuf=(uchar *)malloc(length))==NULL) {
+		close(file);
+		return(FALSE); 
+	}
+	if(read(file,ixbbuf,length)!=length) {
+		close(file);
+		free(ixbbuf);
+		return(FALSE); 
+	}
+	SAFECOPY(fname,f->name);
+	for(l=8;l<12;l++)	/* Turn FILENAME.EXT into FILENAMEEXT */
+		fname[l]=fname[l+1];
+	for(l=0;l<length;l+=F_IXBSIZE) {
+		SAFEPRINTF(str,"%11.11s",ixbbuf+l);
+		if(!stricmp(str,fname))
+			break; 
+	}
+	free(ixbbuf);
+
+	if(l>=length) {
+		close(file);
+		return(FALSE); 
+	}
+	
+	lseek(file,l+11+3,SEEK_SET);
+
+	write(file,&f->dateuled,4);
+	write(file,&f->datedled,4);
+
+	close(file);
+
+	return(TRUE);
+}
+
+int openextdesc(scfg_t* cfg, uint dirnum)
+{
+	char str[MAX_PATH+1];
+	SAFEPRINTF2(str,"%s%s.exb",cfg->dir[dirnum]->data_dir,cfg->dir[dirnum]->code);
+	return nopen(str,O_RDONLY);
+}
+
+void closeextdesc(int file)
+{
+	if(file >= 0)
+		close(file);
+}
+
+void getextdesc(scfg_t* cfg, uint dirnum, ulong datoffset, char *ext)
+{
+	int file;
+
+	memset(ext,0,F_EXBSIZE+1);
+	if((file=openextdesc(cfg, dirnum))==-1)
+		return;
+	lseek(file,(datoffset/F_LEN)*F_EXBSIZE,SEEK_SET);
+	read(file,ext,F_EXBSIZE);
+	close(file);
+}
+
+// fast (operates on open .exb file)
+void fgetextdesc(scfg_t* cfg, uint dirnum, ulong datoffset, char *ext, int file)
+{
+	lseek(file,(datoffset/F_LEN)*F_EXBSIZE,SEEK_SET);
+	read(file,ext,F_EXBSIZE);
+}
+
+void putextdesc(scfg_t* cfg, uint dirnum, ulong datoffset, char *ext)
+{
+	char str[MAX_PATH+1],nulbuf[F_EXBSIZE];
+	int file;
+
+	strip_ansi(ext);
+	strip_invalid_attr(ext);	/* eliminate bogus ctrl-a codes */
+	memset(nulbuf,0,sizeof(nulbuf));
+	SAFEPRINTF2(str,"%s%s.exb",cfg->dir[dirnum]->data_dir,cfg->dir[dirnum]->code);
+	if((file=nopen(str,O_WRONLY|O_CREAT))==-1)
+		return;
+	lseek(file,0L,SEEK_END);
+	while(filelength(file)<(long)(datoffset/F_LEN)*F_EXBSIZE)
+		write(file,nulbuf,sizeof(nulbuf));
+	lseek(file,(datoffset/F_LEN)*F_EXBSIZE,SEEK_SET);
+	write(file,ext,F_EXBSIZE);
+	close(file);
+}
+
+/****************************************************************************/
+/* Update the upload date for the file 'f'                                  */
+/****************************************************************************/
+int update_uldate(scfg_t* cfg, oldfile_t* f)
+{
+	char str[MAX_PATH+1],fname[13];
+	int i,file;
+	long l,length;
+
+	/*******************/
+	/* Update IXB File */
+	/*******************/
+	SAFEPRINTF2(str,"%s%s.ixb",cfg->dir[f->dir]->data_dir,cfg->dir[f->dir]->code);
+	if((file=nopen(str,O_RDWR))==-1)
+		return(errno); 
+	length=(long)filelength(file);
+	if(length%F_IXBSIZE) {
+		close(file);
+		return(-1); 
+	}
+	SAFECOPY(fname,f->name);
+	for(i=8;i<12;i++)   /* Turn FILENAME.EXT into FILENAMEEXT */
+		fname[i]=fname[i+1];
+	for(l=0;l<length;l+=F_IXBSIZE) {
+		read(file,str,F_IXBSIZE);      /* Look for the filename in the IXB file */
+		str[11]=0;
+		if(!stricmp(fname,str)) break; 
+	}
+	if(l>=length) {
+		close(file);
+		return(-2); 
+	}
+	lseek(file,l+14,SEEK_SET);
+	write(file,&f->dateuled,4);
+	close(file);
+
+	/*******************************************/
+	/* Update last upload date/time stamp file */
+	/*******************************************/
+	SAFEPRINTF2(str,"%s%s.dab",cfg->dir[f->dir]->data_dir,cfg->dir[f->dir]->code);
+	if((file=nopen(str,O_WRONLY|O_CREAT))==-1)
+		return(errno);
+
+	write(file,&f->dateuled,4);
+	close(file); 
+	return(0);
+}
+
+bool upgrade_file_bases(bool hash)
+{
+	int result;
+	ulong total_files = 0;
+	time_t start = time(NULL);
+
+	printf("Upgrading File Bases...\n");
+
+	for(int i = 0; i < scfg.total_dirs; i++) {
+		smb_t smb;
+
+		SAFEPRINTF2(smb.file, "%s%s", scfg.dir[i]->data_dir, scfg.dir[i]->code);
+		if((result = smb_open(&smb)) != SMB_SUCCESS) {
+			fprintf(stderr, "Error %d (%s) opening %s\n", result, smb.last_error, smb.file);
+			return false;
+		}
+		smb.status.attr = SMB_FILE_DIRECTORY;
+		if(!hash || (scfg.dir[i]->misc & DIR_NOHASH))
+			smb.status.attr |= SMB_NOHASH;
+		smb.status.max_age = scfg.dir[i]->maxage;
+		smb.status.max_msgs = scfg.dir[i]->maxfiles;
+		if((result = smb_create(&smb)) != SMB_SUCCESS) {
+			fprintf(stderr, "Error %d (%s) creating %s\n", result, smb.last_error, smb.file);
+			return false;
+		}
+
+		char str[MAX_PATH+1];
+		int file;
+		int extfile = openextdesc(&scfg, i);
+
+		sprintf(str,"%s%s.ixb",scfg.dir[i]->data_dir,scfg.dir[i]->code);
+		if((file=open(str,O_RDONLY|O_BINARY))==-1) {
+			smb_close(&smb);
+			continue;
+		}
+		long l=(long)filelength(file);
+		if(!l) {
+			close(file);
+			smb_close(&smb);
+			continue;
+		}
+		uchar* ixbbuf;
+		if((ixbbuf=(uchar *)malloc(l))==NULL) {
+			close(file);
+			printf("\7ERR_ALLOC %s %lu\n",str,l);
+			smb_close(&smb);
+			continue;
+		}
+		if(read(file,ixbbuf,l)!=(int)l) {
+			close(file);
+			printf("\7ERR_READ %s %lu\n",str,l);
+			free(ixbbuf);
+			smb_close(&smb);
+			continue;
+		}
+		close(file);
+		size_t file_count = l / F_IXBSIZE;
+		oldfile_t* filelist = malloc(sizeof(*filelist) * file_count);
+		if(filelist == NULL) {
+			printf("malloc failure");
+			return false;
+		}
+		memset(filelist, 0, sizeof(*filelist) * file_count);
+		oldfile_t* f = filelist;
+		long m=0L;
+		while(m + F_IXBSIZE <= l) {
+			int j;
+			f->dir = i;
+			for(j=0;j<12 && m<l;j++)
+				if(j==8)
+					f->name[j]=ixbbuf[m]>' ' ? '.' : ' ';
+				else
+					f->name[j]=ixbbuf[m++]; /* Turns FILENAMEEXT into FILENAME.EXT */
+			f->name[j]=0;
+			f->datoffset=ixbbuf[m]|((long)ixbbuf[m+1]<<8)|((long)ixbbuf[m+2]<<16);
+			f->dateuled=(ixbbuf[m+3]|((long)ixbbuf[m+4]<<8)|((long)ixbbuf[m+5]<<16)
+				|((long)ixbbuf[m+6]<<24));
+			f->datedled =(ixbbuf[m+7]|((long)ixbbuf[m+8]<<8)|((long)ixbbuf[m+9]<<16)
+				|((long)ixbbuf[m+10]<<24));
+			m+=11;
+			f++;
+		};
+
+		/* SMB index is sorted by import (upload) time */
+		qsort(filelist, file_count, sizeof(*filelist), file_uldate_compare);
+
+		time_t latest = 0;
+		for(size_t fi = 0; fi < file_count; fi++) {
+			f = &filelist[fi];
+			if(!getfiledat(&scfg, f)) {
+				fprintf(stderr, "\nError getting file data for %s %s\n", scfg.dir[i]->code, f->name);
+				continue;
+			}
+			char fpath[MAX_PATH+1];
+			getoldfilepath(&scfg, f, fpath);
+			file_t file;
+			memset(&file, 0, sizeof(file));
+			file.hdr.when_imported.time = f->dateuled;
+			file.hdr.last_downloaded = f->datedled;
+			file.hdr.times_downloaded = f->timesdled;
+			smb_hfield_str(&file, SMB_FILENAME, getfname(fpath));
+			smb_hfield_str(&file, SMB_FILEDESC, f->desc);
+			smb_hfield_str(&file, SENDER, f->uler);
+			smb_hfield_bin(&file, SMB_COST, f->cdt);
+			if(f->misc&FM_ANON)
+				file.hdr.attr |= FILE_ANONYMOUS;
+			{
+				const char* body = NULL;
+				char extdesc[F_EXBSIZE+1] = {0};
+				if(f->misc&FM_EXTDESC && extfile > 0) {
+					fgetextdesc(&scfg, i, f->datoffset, extdesc, extfile);
+					truncsp(extdesc);
+					if(*extdesc)
+						body = extdesc;
+				}
+				result = smb_addfile(&smb, &file, SMB_FASTALLOC, body, fpath);
+			}
+			if(result != SMB_SUCCESS) {
+				fprintf(stderr, "\n!Error %d (%s) adding file to %s\n", result, smb.last_error, smb.file);
+			} else {
+				total_files++;
+				time_t diff = time(NULL) - start;
+				printf("\r%-16s (%-5u bases remain) %lu files imported (%lu files/second)"
+					, scfg.dir[i]->code, scfg.total_dirs - (i + 1), total_files, (ulong)(diff ? total_files / diff : total_files));
+			}
+			if(f->dateuled > latest)
+				latest = f->dateuled;
+			smb_freefilemem(&file);
+		}
+		free(filelist);
+		off_t new_count = filelength(fileno(smb.sid_fp)) / smb_idxreclen(&smb);
+		smb_close(&smb);
+		if(latest > 0)
+			update_newfiletime(&smb, latest);
+		closeextdesc(extfile);
+		free(ixbbuf);
+		if(new_count != file_count) {
+			printf("\n%s: New file base index has %u records instead of %u\n"
+				,scfg.dir[i]->code, (uint)new_count, (uint)file_count);
+		}
+	}
+	time_t diff = time(NULL) - start;
+	printf("\r%lu files imported in %u directories (%lu files/second)%40s\n"
+		,total_files, scfg.total_dirs, (ulong)(diff ? total_files / diff : total_files), "");
+
+	return true;
+}
+
+char *usage="\nusage: upgrade [ctrl_dir]\n";
+
+int main(int argc, char** argv)
+{
+	char	error[512];
+
+	fprintf(stderr,"\nupgrade - Upgrade Synchronet BBS to %s\n"
+		,VERSION
+		);
+
+	memset(&scfg, 0, sizeof(scfg));
+	scfg.size = sizeof(scfg);
+	SAFECOPY(scfg.ctrl_dir, get_ctrl_dir(/* warn: */true));
+
+	if(chdir(scfg.ctrl_dir) != 0)
+		fprintf(stderr,"!ERROR changing directory to: %s", scfg.ctrl_dir);
+
+	printf("\nLoading configuration files from %s\n",scfg.ctrl_dir);
+	if(!load_cfg(&scfg, NULL, TRUE, /* node: **/FALSE, error, sizeof(error))) {
+		fprintf(stderr,"!ERROR loading configuration files: %s\n",error);
+		return EXIT_FAILURE + __COUNTER__;
+	}
+
+	if(!upgrade_file_bases(/* hash: */true))
+		return EXIT_FAILURE + __COUNTER__;
+
+	printf("Upgrade successful.\n");
+    return EXIT_SUCCESS;
+}
diff --git a/src/sbbs3/upgrade_to_v319.vcxproj b/src/sbbs3/upgrade_to_v319.vcxproj
new file mode 100644
index 0000000000000000000000000000000000000000..c65d3d982651c47275ba47d1b9426825b2514c85
--- /dev/null
+++ b/src/sbbs3/upgrade_to_v319.vcxproj
@@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <ItemGroup Label="ProjectConfigurations">
+    <ProjectConfiguration Include="Debug|Win32">
+      <Configuration>Debug</Configuration>
+      <Platform>Win32</Platform>
+    </ProjectConfiguration>
+    <ProjectConfiguration Include="Release|Win32">
+      <Configuration>Release</Configuration>
+      <Platform>Win32</Platform>
+    </ProjectConfiguration>
+  </ItemGroup>
+  <PropertyGroup Label="Globals">
+    <VCProjectVersion>16.0</VCProjectVersion>
+    <Keyword>Win32Proj</Keyword>
+    <ProjectGuid>{b84cb739-8425-4612-bdef-b292beae858f}</ProjectGuid>
+    <RootNamespace>upgrade</RootNamespace>
+    <WindowsTargetPlatformVersion>7.0</WindowsTargetPlatformVersion>
+  </PropertyGroup>
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
+    <ConfigurationType>Application</ConfigurationType>
+    <UseDebugLibraries>true</UseDebugLibraries>
+    <PlatformToolset>v141_xp</PlatformToolset>
+    <CharacterSet>MultiByte</CharacterSet>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
+    <ConfigurationType>Application</ConfigurationType>
+    <UseDebugLibraries>false</UseDebugLibraries>
+    <PlatformToolset>v141_xp</PlatformToolset>
+    <WholeProgramOptimization>true</WholeProgramOptimization>
+    <CharacterSet>MultiByte</CharacterSet>
+  </PropertyGroup>
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
+  <ImportGroup Label="ExtensionSettings">
+  </ImportGroup>
+  <ImportGroup Label="Shared">
+  </ImportGroup>
+  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
+    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+    <Import Project="..\xpdev\xpdev.props" />
+    <Import Project="..\smblib\smblib.props" />
+    <Import Project="..\hash\hash.props" />
+    <Import Project="..\build\undeprecate.props" />
+    <Import Project="..\..\3rdp\win32.release\libarchive\libarchive.props" />
+  </ImportGroup>
+  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
+    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+    <Import Project="..\xpdev\xpdev.props" />
+    <Import Project="..\smblib\smblib.props" />
+    <Import Project="..\hash\hash.props" />
+    <Import Project="..\build\undeprecate.props" />
+    <Import Project="..\..\3rdp\win32.release\libarchive\libarchive.props" />
+  </ImportGroup>
+  <PropertyGroup Label="UserMacros" />
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
+    <LinkIncremental>true</LinkIncremental>
+    <OutDir>.\msvc.win32.exe.debug\</OutDir>
+    <IntDir>.\msvc.win32.debug\upgrade\</IntDir>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
+    <LinkIncremental>false</LinkIncremental>
+    <OutDir>.\msvc.win32.exe.release\</OutDir>
+    <IntDir>.\msvc.win32.release\upgrade\</IntDir>
+  </PropertyGroup>
+  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
+    <ClCompile>
+      <WarningLevel>Level3</WarningLevel>
+      <SDLCheck>true</SDLCheck>
+      <PreprocessorDefinitions>SBBS_EXPORTS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <ConformanceMode>true</ConformanceMode>
+    </ClCompile>
+    <Link>
+      <SubSystem>Console</SubSystem>
+      <GenerateDebugInformation>true</GenerateDebugInformation>
+    </Link>
+  </ItemDefinitionGroup>
+  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
+    <ClCompile>
+      <WarningLevel>Level3</WarningLevel>
+      <FunctionLevelLinking>true</FunctionLevelLinking>
+      <IntrinsicFunctions>true</IntrinsicFunctions>
+      <SDLCheck>true</SDLCheck>
+      <PreprocessorDefinitions>SBBS_EXPORTS;WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <ConformanceMode>true</ConformanceMode>
+    </ClCompile>
+    <Link>
+      <SubSystem>Console</SubSystem>
+      <EnableCOMDATFolding>true</EnableCOMDATFolding>
+      <OptimizeReferences>true</OptimizeReferences>
+      <GenerateDebugInformation>true</GenerateDebugInformation>
+    </Link>
+  </ItemDefinitionGroup>
+  <ItemGroup>
+    <ClCompile Include="dat_rec.c" />
+    <ClCompile Include="filedat.c" />
+    <ClCompile Include="upgrade_to_v319.c" />
+    <ClCompile Include="userdat.c" />
+  </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\smblib\smblib.vcxproj">
+      <Project>{d674842b-2f41-42cb-9426-b3c4b0682574}</Project>
+    </ProjectReference>
+    <ProjectReference Include="..\xpdev\xpdev.vcxproj">
+      <Project>{7428a1e8-56b7-4868-9c0e-29d031689feb}</Project>
+    </ProjectReference>
+    <ProjectReference Include="load_cfg.vcxproj">
+      <Project>{08fc395f-bc60-499d-9ce9-170ed718bb94}</Project>
+    </ProjectReference>
+  </ItemGroup>
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
+  <ImportGroup Label="ExtensionTargets">
+  </ImportGroup>
+</Project>
\ No newline at end of file
diff --git a/src/sbbs3/upload.cpp b/src/sbbs3/upload.cpp
index 13e6c2a9f50023695e82f72b8cb3819aabf0dc08..c3431ab4926d18bf2febc4d9978fd6c2d4f52c42 100644
--- a/src/sbbs3/upload.cpp
+++ b/src/sbbs3/upload.cpp
@@ -1,9 +1,5 @@
-/* upload.cpp */
-
 /* Synchronet file upload-related routines */
 
-/* $Id: upload.cpp,v 1.63 2019/08/02 10:36:45 rswindell Exp $ */
-
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
  * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
@@ -17,89 +13,69 @@
  * See the GNU General Public License for more details: gpl.txt or			*
  * http://www.fsf.org/copyleft/gpl.html										*
  *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
- *																			*
  * For Synchronet coding style and modification guidelines, see				*
  * http://www.synchro.net/source.html										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
 #include "sbbs.h"
+#include "filedat.h"
 
 /****************************************************************************/
-/* Adds file data in 'f' to DIR#.DAT and DIR#.IXB   and updates user        */
-/* info for uploader. Must have .name, .desc and .dir fields filled prior   */
-/* to a call to this function.                                              */
-/* Returns 1 if file uploaded sucessfully, 0 if not.                        */
 /****************************************************************************/
-bool sbbs_t::uploadfile(file_t *f)
+bool sbbs_t::uploadfile(file_t* f)
 {
 	char	path[MAX_PATH+1];
-	char	str[MAX_PATH+1],fname[25];
-	char	ext[F_EXBSIZE+1];
-	char	desc[F_EXBSIZE+1];
+	char	str[MAX_PATH+1] = "";
+	char	ext[LEN_EXTDESC + 1] = "";
 	char	tmp[MAX_PATH+1];
-	int		file;
     uint	i;
     long	length;
 	FILE*	stream;
 
-	// f->misc=0; Removed AUG-22-2001 - broken anonymous uploads
 	curdirnum=f->dir;
-	if(findfile(&cfg,f->dir,f->name)) {
-		errormsg(WHERE,ERR_CHK,f->name,f->dir);
-		return(0); 
+	if(findfile(&cfg, f->dir, f->name, NULL)) {
+		errormsg(WHERE, ERR_CHK, f->name, f->dir);
+		return false;
 	}
-	sprintf(path,"%s%s",f->altpath>0 && f->altpath<=cfg.altpaths
-		? cfg.altpath[f->altpath-1]
-		: cfg.dir[f->dir]->path,unpadfname(f->name,fname));
-	sprintf(tmp,"%s%s",cfg.temp_dir,fname);
+	getfilepath(&cfg, f, path);
+	SAFEPRINTF2(tmp, "%s%s", cfg.temp_dir, getfname(path));
 	if(!fexistcase(path) && fexistcase(tmp))
 		mv(tmp,path,0);
 	if(!fexistcase(path)) {
 		bprintf(text[FileNotReceived],f->name);
-		sprintf(str,"attempted to upload %s to %s %s (Not received)"
+		safe_snprintf(str,sizeof(str),"attempted to upload %s to %s %s (Not received)"
 			,f->name
 			,cfg.lib[cfg.dir[f->dir]->lib]->sname,cfg.dir[f->dir]->sname);
 		logline(LOG_NOTICE,"U!",str);
-		return(0); 
+		return false;
 	}
-	strcpy(tmp,f->name);
-	truncsp(tmp);
+	f->hdr.when_written.time = (uint32_t)fdate(path);
+	char* fext = getfext(f->name);
 	for(i=0;i<cfg.total_ftests;i++)
-		if(cfg.ftest[i]->ext[0]=='*' || !stricmp(tmp+9,cfg.ftest[i]->ext)) {
+		if(cfg.ftest[i]->ext[0]=='*' || (fext != NULL && stricmp(fext, cfg.ftest[i]->ext) == 0)) {
 			if(!chk_ar(cfg.ftest[i]->ar,&useron,&client))
 				continue;
 			attr(LIGHTGRAY);
 			bputs(cfg.ftest[i]->workstr);
 
-			sprintf(sbbsfilename,"SBBSFILENAME=%.64s",unpadfname(f->name,fname));
+			safe_snprintf(sbbsfilename,sizeof(sbbsfilename),"SBBSFILENAME=%s",f->name);
 			putenv(sbbsfilename);
-			sprintf(sbbsfiledesc,"SBBSFILEDESC=%.*s",LEN_FDESC,f->desc);
+			safe_snprintf(sbbsfiledesc,sizeof(sbbsfiledesc),"SBBSFILEDESC=%s",f->desc);
 			putenv(sbbsfiledesc);
-			sprintf(str,"%ssbbsfile.nam",cfg.node_dir);
+			SAFEPRINTF(str,"%ssbbsfile.nam",cfg.node_dir);
 			if((stream=fopen(str,"w"))!=NULL) {
-				fwrite(fname,1,strlen(fname),stream);
+				fprintf(stream, "%s", f->desc);
 				fclose(stream); 
 			}
-			sprintf(str,"%ssbbsfile.des",cfg.node_dir);
+			SAFEPRINTF(str,"%ssbbsfile.des",cfg.node_dir);
 			if((stream=fopen(str,"w"))!=NULL) {
 				fwrite(f->desc,1,strlen(f->desc),stream);
 				fclose(stream); 
 			}
 			if(external(cmdstr(cfg.ftest[i]->cmd,path,f->desc,NULL),EX_OFFLINE)) {
-				sprintf(str,"attempted to upload %s to %s %s (%s Errors)"
+				safe_snprintf(str,sizeof(str),"attempted to upload %s to %s %s (%s Errors)"
 					,f->name
 					,cfg.lib[cfg.dir[f->dir]->lib]->sname,cfg.dir[f->dir]->sname,cfg.ftest[i]->ext);
 				logline(LOG_NOTICE,"U!",str);
@@ -112,6 +88,7 @@ bool sbbs_t::uploadfile(file_t *f)
 					remove(path);
 				return(0); 
 			} else {
+#if 0 // NFB-TODO - uploader tester changes filename or description
 				sprintf(str,"%ssbbsfile.nam",cfg.node_dir);
 				if((stream=fopen(str,"r"))!=NULL) {
 					if(fgets(str,128,stream)) {
@@ -119,8 +96,7 @@ bool sbbs_t::uploadfile(file_t *f)
 						padfname(str,f->name);
 						strcpy(tmp,f->name);
 						truncsp(tmp);
-						sprintf(path,"%s%s",f->altpath>0 && f->altpath<=cfg.altpaths
-							? cfg.altpath[f->altpath-1] : cfg.dir[f->dir]->path
+						sprintf(path,"%s%s", cfg.dir[f->dir]->path
 							,unpadfname(f->name,fname)); 
 					}
 					fclose(stream);
@@ -133,56 +109,71 @@ bool sbbs_t::uploadfile(file_t *f)
 					}
 					fclose(stream); 
 				}
-				CRLF; 
-			} 
+				CRLF;
+#endif
+			}
 		}
 
 	if((length=(long)flength(path))==0L) {
 		bprintf(text[FileZeroLength],f->name);
 		remove(path);
-		sprintf(str,"attempted to upload %s to %s %s (Zero length)"
+		safe_snprintf(str,sizeof(str),"attempted to upload %s to %s %s (Zero length)"
 			,f->name
 			,cfg.lib[cfg.dir[f->dir]->lib]->sname,cfg.dir[f->dir]->sname);
 		logline(LOG_NOTICE,"U!",str);
-		return(0); 
+		return false; 
 	}
-	if(cfg.dir[f->dir]->misc&DIR_DIZ) {
-		for(i=0;i<cfg.total_fextrs;i++)
-			if(!stricmp(cfg.fextr[i]->ext,tmp+9) && chk_ar(cfg.fextr[i]->ar,&useron,&client))
-				break;
-		if(i<cfg.total_fextrs) {
-			sprintf(str,"%sFILE_ID.DIZ",cfg.temp_dir);
-			if(fexistcase(str))
-				remove(str);
-			external(cmdstr(cfg.fextr[i]->cmd,path,"FILE_ID.DIZ",NULL),EX_OFFLINE);
-			if(!fexistcase(str)) {
-				sprintf(str,"%sDESC.SDI",cfg.temp_dir);
-				if(fexistcase(str))
-					remove(str);
-				external(cmdstr(cfg.fextr[i]->cmd,path,"DESC.SDI",NULL),EX_OFFLINE); 
-				fexistcase(str);
+
+	bputs(text[SearchingForDupes]);
+	/* Note: Hashes file *after* running upload-testers (which could modify file) */
+	if(hashfile(&cfg, f)) {
+		bputs(text[SearchedForDupes]);
+		for(uint i=0, k=0; i < usrlibs; i++) {
+			progress(text[Scanning], i, usrlibs, 1);
+			for(uint j=0; j < usrdirs[i]; j++,k++) {
+				if(cfg.dir[usrdir[i][j]]->misc&DIR_DUPES
+					&& findfile(&cfg, usrdir[i][j], /* filename: */NULL, f)) {
+					bprintf(text[FileAlreadyOnline], f->name);
+					if(!dir_op(f->dir)) {
+						remove(path);
+						safe_snprintf(str, sizeof(str), "attempted to upload %s to %s %s (duplicate hash)"
+							,f->name
+							,cfg.lib[cfg.dir[f->dir]->lib]->sname, cfg.dir[f->dir]->sname);
+						logline(LOG_NOTICE, "U!", str);
+						return(false); 	 /* File is in database for another dir */
+					}
+				}
 			}
-			if((file=nopen(str,O_RDONLY))!=-1) {
-				memset(ext,0,F_EXBSIZE+1);
-				read(file,ext,F_EXBSIZE);
-				for(i=F_EXBSIZE;i;i--)
-					if(ext[i-1]>' ')
+		}
+		progress(text[Done], usrlibs, usrlibs);
+	} else
+		bputs(text[SearchedForDupes]);
+
+	if(cfg.dir[f->dir]->misc&DIR_DIZ) {
+		lprintf(LOG_DEBUG, "Extracting DIZ from: %s", path);
+		if(extract_diz(&cfg, f, /* diz_fnames: */NULL, str, sizeof(str))) {
+			lprintf(LOG_DEBUG, "Parsing DIZ: %s", str);
+
+			str_list_t lines = read_diz(str, /* max_line_len: */80);
+			if(lines != NULL)
+				format_diz(lines, ext, sizeof(ext), /* allow_ansi: */false);
+			strListFree(&lines);
+
+			if(f->desc == NULL || f->desc[0] == 0) {
+				char	desc[LEN_FDESC];
+				SAFECOPY(desc, (char*)ext);
+				strip_exascii(desc, desc);
+				prep_file_desc(desc, desc);
+				for(i=0;desc[i];i++)
+					if(IS_ALPHANUMERIC(desc[i]))
 						break;
-				ext[i]=0;
-				if(!f->desc[0]) {
-					strcpy(desc,ext);
-					strip_exascii(desc, desc);
-					prep_file_desc(desc, desc);
-					for(i=0;desc[i];i++)
-						if(IS_ALPHANUMERIC(desc[i]))
-							break;
-					sprintf(f->desc,"%.*s",LEN_FDESC,desc+i); 
-				}
-				close(file);
-				remove(str);
-				f->misc|=FM_EXTDESC; 
-			} 
-		} 
+				if(desc[i] == '\0')
+					i = 0;
+				smb_hfield_str(f, SMB_FILEDESC, desc + i);
+			}
+			remove(str);
+		} else
+			lprintf(LOG_DEBUG, "DIZ does not exist in: %s", path);
 	}
 
 	if(!(cfg.dir[f->dir]->misc&DIR_NOSTAT)) {
@@ -190,27 +181,15 @@ bool sbbs_t::uploadfile(file_t *f)
 		logon_uls++;
 	}
 	if(cfg.dir[f->dir]->misc&DIR_AONLY)  /* Forced anonymous */
-		f->misc|=FM_ANON;
-	f->cdt=length;
-	f->dateuled=time32(NULL);
-	f->timesdled=0;
-	f->datedled=0L;
-	f->opencount=0;
-	strcpy(f->uler,useron.alias);
+		f->hdr.attr |= MSG_ANONYMOUS;
+	uint32_t cdt = (uint32_t)length;
+	smb_hfield_bin(f, SMB_COST, cdt);
+	smb_hfield_str(f, SENDER, useron.alias);
 	bprintf(text[FileNBytesReceived],f->name,ultoac(length,tmp));
-	if(!f->desc[0]) {
-		if(stricmp(f->name,getfname(path)))
-			SAFECOPY(f->desc,getfname(path));
-		else
-			sprintf(f->desc,"%.*s",LEN_FDESC,text[NoDescription]);
-	}
-	if(!addfiledat(&cfg,f))
-		return(0);
+	if(!addfile(&cfg, f->dir, f, ext))
+		return false;
 
-	if(f->misc&FM_EXTDESC)
-		putextdesc(&cfg,f->dir,f->datoffset,ext);
-
-	sprintf(str,"uploaded %s to %s %s"
+	safe_snprintf(str,sizeof(str),"uploaded %s to %s %s"
 		,f->name,cfg.lib[cfg.dir[f->dir]->lib]->sname
 		,cfg.dir[f->dir]->sname);
 	if(cfg.dir[f->dir]->upload_sem[0])
@@ -226,12 +205,12 @@ bool sbbs_t::uploadfile(file_t *f)
 				,((ulong)(length*(cfg.dir[f->dir]->up_pct/100.0))/cur_cps)/60);
 		else
 			useron.cdt=adjustuserrec(&cfg,useron.number,U_CDT,10
-				,(ulong)(f->cdt*(cfg.dir[f->dir]->up_pct/100.0))); 
+				,(ulong)(f->cost * (cfg.dir[f->dir]->up_pct/100.0))); 
 	}
 
 	user_event(EVENT_UPLOAD);
 
-	return(true);
+	return true;
 }
 
 /****************************************************************************/
@@ -240,18 +219,14 @@ bool sbbs_t::uploadfile(file_t *f)
 bool sbbs_t::upload(uint dirnum)
 {
 	char	descbeg[25]={""},descend[25]={""}
-				,fname[13],keys[256],ch,*p;
+				,fname[MAX_FILENAME_LEN + 1],keys[256],ch,*p;
 	char	str[MAX_PATH+1];
 	char	path[MAX_PATH+1];
-	char	spath[MAX_PATH+1];
 	char 	tmp[512];
     time_t	start,end;
-    uint	i,j,k,destuser[MAX_USERXFER],destusers=0;
-	int		file;
+    uint	i,j,k;
 	ulong	space;
-    file_t	f;
-    user_t	user;
-    node_t	node;
+	file_t	f = {{}};
 
 	/* Security Checks */
 	if(useron.rest&FLAG('U')) {
@@ -276,10 +251,8 @@ bool sbbs_t::upload(uint dirnum)
 
 	if(sys_status&SS_EVENT && online==ON_REMOTE && !dir_op(dirnum))
 		bprintf(text[UploadBeforeEvent],timeleft/60);
-	if(altul)
-		strcpy(path,cfg.altpath[altul-1]);
-	else
-		strcpy(path,cfg.dir[dirnum]->path);
+
+	SAFECOPY(path,cfg.dir[dirnum]->path);
 
 	if(!isdir(path)) {
 		bprintf(text[DirectoryDoesNotExist], path);
@@ -298,32 +271,25 @@ bool sbbs_t::upload(uint dirnum)
 	bprintf(text[DiskNBytesFree],ultoac(space,tmp));
 
 	f.dir=curdirnum=dirnum;
-	f.misc=0;
-	f.altpath=altul;
 	bputs(text[Filename]);
-	if(!getstr(fname,12,0) || strchr(fname,'?') || strchr(fname,'*')
+	if(getstr(fname, sizeof(fname) - 1, 0) < 1 || strchr(fname,'?') || strchr(fname,'*')
 		|| !checkfname(fname) || (trashcan(fname,"file") && !dir_op(dirnum))) {
 		if(fname[0])
 			bputs(text[BadFilename]);
 		return(false); 
 	}
 	if(dirnum==cfg.sysop_dir)
-		sprintf(str,text[UploadToSysopDirQ],fname);
+		SAFEPRINTF(str,text[UploadToSysopDirQ],fname);
 	else if(dirnum==cfg.user_dir)
-		sprintf(str,text[UploadToUserDirQ],fname);
+		SAFEPRINTF(str,text[UploadToUserDirQ],fname);
 	else
-		sprintf(str,text[UploadToCurDirQ],fname,cfg.lib[cfg.dir[dirnum]->lib]->sname
+		SAFEPRINTF3(str,text[UploadToCurDirQ],fname,cfg.lib[cfg.dir[dirnum]->lib]->sname
 			,cfg.dir[dirnum]->sname);
 	if(!yesno(str)) return(false);
 	action=NODE_ULNG;
-	SAFEPRINTF2(str,"%s%s",path,fname);
-	if(fexistcase(str)) {   /* File is on disk */
-#ifdef _WIN32
-		GetShortPathName(str, spath, sizeof(spath));
-#else
-		SAFECOPY(spath,str);
-#endif
-		SAFECOPY(fname,getfname(spath));
+	SAFECOPY(f.file_idx.name, fname);
+	getfilepath(&cfg, &f, path);
+	if(fexistcase(path)) {   /* File is on disk */
 		if(!dir_op(dirnum) && online!=ON_LOCAL) {		 /* local users or sysops */
 			bprintf(text[FileAlreadyThere],fname);
 			return(false); 
@@ -331,18 +297,16 @@ bool sbbs_t::upload(uint dirnum)
 		if(!yesno(text[FileOnDiskAddQ]))
 			return(false); 
 	}
-	padfname(fname,f.name);
-	strcpy(str,cfg.dir[dirnum]->exts);
-	strcpy(tmp,f.name);
-	truncsp(tmp);
+	char* ext = getfext(fname);
+	SAFECOPY(str,cfg.dir[dirnum]->exts);
 	j=strlen(str);
 	for(i=0;i<j;i+=ch+1) { /* Check extension of upload with allowable exts */
 		p=strchr(str+i,',');
 		if(p!=NULL)
 			*p=0;
 		ch=(char)strlen(str+i);
-		if(!stricmp(tmp+9,str+i))
-			break; 
+		if(ext != NULL && stricmp(ext + 1, str + i) == 0)
+			break;
 	}
 	if(j && i>=j) {
 		bputs(text[TheseFileExtsOnly]);
@@ -351,20 +315,19 @@ bool sbbs_t::upload(uint dirnum)
 		if(!dir_op(dirnum)) return(false); 
 	}
 	bputs(text[SearchingForDupes]);
-	if(findfile(&cfg,dirnum,f.name)) {
-		bputs(text[SearchedForDupes]);
+	bool found = findfile(&cfg, dirnum, fname, NULL);
+	bputs(text[SearchedForDupes]);
+	if(found) {
 		bprintf(text[FileAlreadyOnline],fname);
 		return(false); 	 /* File is already in database */
 	}
 	for(i=k=0;i<usrlibs;i++) {
+		progress(text[SearchingForDupes], i, usrlibs, 1);
 		for(j=0;j<usrdirs[i];j++,k++) {
-			outchar('.');
-			if(k && !(k%5))
-				bputs("\b\b\b\b\b     \b\b\b\b\b");
 			if(usrdir[i][j]==dirnum)
 				continue;	/* we already checked this dir */
 			if(cfg.dir[usrdir[i][j]]->misc&DIR_DUPES
-				&& findfile(&cfg,usrdir[i][j],f.name)) {
+				&& findfile(&cfg, usrdir[i][j], fname, NULL)) {
 				bputs(text[SearchedForDupes]);
 				bprintf(text[FileAlreadyOnline],fname);
 				if(!dir_op(dirnum))
@@ -373,41 +336,6 @@ bool sbbs_t::upload(uint dirnum)
 		}
 	}
 	bputs(text[SearchedForDupes]);
-	if(dirnum==cfg.user_dir) {  /* User to User transfer */
-		bputs(text[EnterAfterLastDestUser]);
-		while((!dir_op(dirnum) && destusers<cfg.max_userxfer) || destusers<MAX_USERXFER) {
-			bputs(text[SendFileToUser]);
-			if(!getstr(str,LEN_ALIAS,cfg.uq&UQ_NOUPRLWR ? K_NONE:K_UPRLWR))
-				break;
-			if((user.number=finduser(str))!=0) {
-				if(!dir_op(dirnum) && user.number==useron.number) {
-					bputs(text[CantSendYourselfFiles]);
-					continue; 
-				}
-				for(i=0;i<destusers;i++)
-					if(user.number==destuser[i])
-						break;
-				if(i<destusers) {
-					bputs(text[DuplicateUser]);
-					continue; 
-				}
-				getuserdat(&cfg,&user);
-				if((user.rest&(FLAG('T')|FLAG('D')))
-					|| !chk_ar(cfg.lib[cfg.dir[cfg.user_dir]->lib]->ar,&user,/* client: */NULL)
-					|| !chk_ar(cfg.dir[cfg.user_dir]->dl_ar,&user,/* client: */NULL)) {
-					bprintf(text[UserWontBeAbleToDl],user.alias); 
-				} else {
-					bprintf(text[UserAddedToDestList],user.alias);
-					destuser[destusers++]=user.number; 
-				} 
-			}
-			else {
-				CRLF; 
-			} 
-		}
-		if(!destusers)
-			return(false); 
-	}
 	if(cfg.dir[dirnum]->misc&DIR_RATE) {
 		SYNC;
 		bputs(text[RateThisFile]);
@@ -415,13 +343,13 @@ bool sbbs_t::upload(uint dirnum)
 		if(!IS_ALPHA(ch) || sys_status&SS_ABORT)
 			return(false);
 		CRLF;
-		sprintf(descbeg,text[Rated],toupper(ch)); 
+		SAFEPRINTF(descbeg,text[Rated],toupper(ch)); 
 	}
 	if(cfg.dir[dirnum]->misc&DIR_ULDATE) {
 		now=time(NULL);
 		if(descbeg[0])
 			strcat(descbeg," ");
-		sprintf(str,"%s  ",unixtodstr(&cfg,(time32_t)now,tmp));
+		SAFEPRINTF(str,"%s  ",unixtodstr(&cfg,(time32_t)now,tmp));
 		strcat(descbeg,str); 
 	}
 	if(cfg.dir[dirnum]->misc&DIR_MULT) {
@@ -436,37 +364,49 @@ bool sbbs_t::upload(uint dirnum)
 			if(j==1)
 				upload_lastdesc[0]=0;
 			if(i>9)
-				sprintf(descend,text[FileOneOfTen],j,i);
+				SAFEPRINTF2(descend,text[FileOneOfTen],j,i);
 			else
-				sprintf(descend,text[FileOneOfTwo],j,i); 
+				SAFEPRINTF2(descend,text[FileOneOfTwo],j,i); 
 		} else
 			upload_lastdesc[0]=0; 
 	}
 	else
 		upload_lastdesc[0]=0;
+
+	char fdesc[LEN_FDESC + 1] = "";
 	bputs(text[EnterDescNow]);
 	i=LEN_FDESC-(strlen(descbeg)+strlen(descend));
-	getstr(upload_lastdesc,i,K_LINE|K_EDIT|K_AUTODEL);
+	getstr(upload_lastdesc,i,K_LINE|K_EDIT|K_AUTODEL|K_TRIM);
 	if(sys_status&SS_ABORT)
 		return(false);
 	if(descend[0])      /* end of desc specified, so pad desc with spaces */
-		sprintf(f.desc,"%s%-*s%s",descbeg,i,upload_lastdesc,descend);
+		safe_snprintf(fdesc,sizeof(fdesc),"%s%-*s%s",descbeg,i,upload_lastdesc,descend);
 	else                /* no end specified, so string ends at desc end */
-		sprintf(f.desc,"%s%s",descbeg,upload_lastdesc);
+		safe_snprintf(fdesc,sizeof(fdesc),"%s%s",descbeg,upload_lastdesc);
+
+	char tags[64] = "";
+	if((cfg.dir[dirnum]->misc&DIR_FILETAGS) && (text[TagFileQ][0] == 0 || !noyes(text[TagFileQ]))) {
+		bputs(text[TagFilePrompt]);
+		getstr(tags, sizeof(tags)-1, K_EDIT|K_LINE|K_TRIM);
+	}
 
 	if(cfg.dir[dirnum]->misc&DIR_ANON && !(cfg.dir[dirnum]->misc&DIR_AONLY)
 		&& (dir_op(dirnum) || useron.exempt&FLAG('A'))) {
 		if(!noyes(text[AnonymousQ]))
-			f.misc|=FM_ANON; 
+			f.hdr.attr |= MSG_ANONYMOUS;
 	}
-	SAFEPRINTF2(str,"%s%s",path,fname);
-	if(fexistcase(str)) {   /* File is on disk */
-		if(!uploadfile(&f))
-			return(false); 
+
+	bool result = false;
+	smb_hfield_str(&f, SMB_FILENAME, fname);
+	smb_hfield_str(&f, SMB_FILEDESC, fdesc);
+	if(tags[0])
+		smb_hfield_str(&f, SMB_TAGS, tags);
+	if(fexistcase(path)) {   /* File is on disk */
+		result = uploadfile(&f);
 	} else {
 		xfer_prot_menu(XFER_UPLOAD);
 		SYNC;
-		sprintf(keys,"%c",text[YNQP][2]);
+		SAFEPRINTF(keys,"%c",text[YNQP][2]);
 		if(dirnum==cfg.user_dir || !cfg.max_batup)  /* no batch user to user xfers */
 			mnemonics(text[ProtocolOrQuit]);
 		else {
@@ -475,30 +415,22 @@ bool sbbs_t::upload(uint dirnum)
 		}
 		for(i=0;i<cfg.total_prots;i++)
 			if(cfg.prot[i]->ulcmd[0] && chk_ar(cfg.prot[i]->ar,&useron,&client)) {
-				sprintf(tmp,"%c",cfg.prot[i]->mnemonic);
+				SAFEPRINTF(tmp,"%c",cfg.prot[i]->mnemonic);
 				strcat(keys,tmp); 
 			}
 		ch=(char)getkeys(keys,0);
 		if(ch==text[YNQP][2] || (sys_status&SS_ABORT))
-			return(false);
-		if(ch=='B') {
-			if(batup_total>=cfg.max_batup)
+			result = false;
+		else if(ch=='B') {
+			if(batup_total() >= cfg.max_batup)
 				bputs(text[BatchUlQueueIsFull]);
-			else {
-				for(i=0;i<batup_total;i++)
-					if(!strcmp(batup_name[i],f.name)) {
-						bprintf(text[FileAlreadyInQueue],fname);
-						return(false); 
-					}
-				strcpy(batup_name[batup_total],f.name);
-				strcpy(batup_desc[batup_total],f.desc);
-				batup_dir[batup_total]=dirnum;
-				batup_misc[batup_total]=f.misc;
-				batup_alt[batup_total]=altul;
-				batup_total++;
+			else if(batch_file_exists(&cfg, useron.number, XFER_BATCH_UPLOAD, f.name))
+				bprintf(text[FileAlreadyInQueue],fname);
+			else if(batch_file_add(&cfg, useron.number, XFER_BATCH_UPLOAD, &f)) {
 				bprintf(text[FileAddedToUlQueue]
-					,fname,batup_total,cfg.max_batup); 
-			} 
+					,fname, batup_total(), cfg.max_batup);
+				result = true;
+			}
 		} else {
 			for(i=0;i<cfg.total_prots;i++)
 				if(cfg.prot[i]->ulcmd[0] && cfg.prot[i]->mnemonic==ch
@@ -506,44 +438,17 @@ bool sbbs_t::upload(uint dirnum)
 					break;
 			if(i<cfg.total_prots) {
 				start=time(NULL);
-				protocol(cfg.prot[i],XFER_UPLOAD,str,nulstr,true);
+				protocol(cfg.prot[i],XFER_UPLOAD,path,nulstr,true);
 				end=time(NULL);
 				if(!(cfg.dir[dirnum]->misc&DIR_ULTIME)) /* Don't deduct upload time */
 					starttime+=end-start;
-				ch=uploadfile(&f);
+				result = uploadfile(&f);
 				autohangup();
-				if(!ch)  /* upload failed, don't process user to user xfer */
-					return(false); 
 			} 
 		} 
 	}
-	if(dirnum==cfg.user_dir) {  /* Add files to XFER.IXT in INDX dir */
-		sprintf(str,"%sxfer.ixt",cfg.data_dir);
-		if((file=nopen(str,O_WRONLY|O_CREAT|O_APPEND))==-1) {
-			errormsg(WHERE,ERR_OPEN,str,O_WRONLY|O_CREAT|O_APPEND);
-			return(false); 
-		}
-		for(j=0;j<destusers;j++) {
-			for(i=1;i<=cfg.sys_nodes;i++) { /* Tell user, if online */
-				getnodedat(i,&node,0);
-				if(node.useron==destuser[j] && !(node.misc&NODE_POFF)
-					&& (node.status==NODE_INUSE || node.status==NODE_QUIET)) {
-					sprintf(str,text[UserToUserXferNodeMsg],cfg.node_num,useron.alias);
-					putnmsg(&cfg,i,str);
-					break; 
-				} 
-			}
-			if(i>cfg.sys_nodes) {   /* User not online */
-				sprintf(str,text[UserSentYouFile],useron.alias);
-				putsmsg(&cfg,destuser[j],str); 
-			}
-			sprintf(str,"%4.4u %12.12s %4.4u\r\n"
-				,destuser[j],f.name,useron.number);
-			write(file,str,strlen(str)); 
-		}
-		close(file); 
-	}
-	return(true);
+	smb_freefilemem(&f);
+	return result;
 }
 
 /****************************************************************************/
@@ -555,21 +460,29 @@ bool sbbs_t::bulkupload(uint dirnum)
 {
     char	str[MAX_PATH+1];
 	char	path[MAX_PATH+1];
-	char	spath[MAX_PATH+1];
-    file_t	f;
+	char	desc[LEN_FDESC + 1];
+	smb_t	smb;
+    file_t f;
 	DIR*	dir;
 	DIRENT*	dirent;
 
-	memset(&f,0,sizeof(file_t));
+	memset(&f,0,sizeof(f));
 	f.dir=dirnum;
-	f.altpath=altul;
 	bprintf(text[BulkUpload],cfg.lib[cfg.dir[dirnum]->lib]->sname,cfg.dir[dirnum]->sname);
-	strcpy(path,altul>0 && altul<=cfg.altpaths ? cfg.altpath[altul-1]
-		: cfg.dir[dirnum]->path);
+	SAFECOPY(path, cfg.dir[dirnum]->path);
+
+	int result = smb_open_dir(&cfg, &smb, dirnum);
+	if(result != SMB_SUCCESS) {
+		errormsg(WHERE, ERR_OPEN, smb.file, result, smb.last_error);
+		return false;
+	}
 	action=NODE_ULNG;
 	SYNC;
+	str_list_t list = loadfilenames(&smb, ALLFILES, /* time_t */0, FILE_SORT_NATURAL, NULL);
+	smb_close(&smb);
 	dir=opendir(path);
 	while(dir!=NULL && (dirent=readdir(dir))!=NULL && !msgabort()) {
+		char fname[SMB_FILEIDX_NAMELEN + 1];
 		SAFEPRINTF2(str,"%s%s",path,dirent->d_name);
 		if(isdir(str))
 			continue;
@@ -578,28 +491,26 @@ bool sbbs_t::bulkupload(uint dirnum)
 		if(getfattr(str)&(_A_HIDDEN|_A_SYSTEM))
 			continue;
 #endif
-#ifdef _WIN32
-		GetShortPathName(str,spath,sizeof(spath));
-#else
-		strcpy(spath,str);
-#endif
-		padfname(getfname(spath),str);
-
-		if(findfile(&cfg,f.dir,str)==0) {
-			f.misc=0;
-			strcpy(f.name,str);
-			f.cdt=(long)flength(spath);
-			bprintf(text[BulkUploadDescPrompt],f.name,f.cdt/1024);
-			getstr(f.desc,LEN_FDESC,K_LINE);
+		smb_fileidxname(dirent->d_name, fname, sizeof(fname));
+		if(strListFind(list, fname, /* case-sensitive: */FALSE) < 0) {
+			smb_freemsgmem(&f);
+			smb_hfield_str(&f, SMB_FILENAME, dirent->d_name);
+			uint32_t cdt = (uint32_t)flength(str);
+			smb_hfield_bin(&f, SMB_COST, cdt);
+			bprintf(text[BulkUploadDescPrompt], format_filename(f.name, fname, 12, /* pad: */FALSE), cdt/1024);
+			getstr(desc, LEN_FDESC, K_LINE);
 			if(sys_status&SS_ABORT)
 				break;
-			if(strcmp(f.desc,"-")==0)	/* don't add this file */
+			if(strcmp(desc,"-")==0)	/* don't add this file */
 				continue;
-			uploadfile(&f); 
+			smb_hfield_str(&f, SMB_FILEDESC, desc);
+			uploadfile(&f);
 		}
 	}
 	if(dir!=NULL)
 		closedir(dir);
+	strListFree(&list);
+	smb_freemsgmem(&f);
 	if(sys_status&SS_ABORT)
 		return(true);
 	return(false);
@@ -617,7 +528,7 @@ bool sbbs_t::recvfile(char *fname, char prot, bool autohang)
 	else {
 		xfer_prot_menu(XFER_UPLOAD);
 		mnemonics(text[ProtocolOrQuit]);
-		sprintf(keys,"%c",text[YNQP][2]);
+		SAFEPRINTF(keys,"%c",text[YNQP][2]);
 		for(i=0;i<cfg.total_prots;i++)
 			if(cfg.prot[i]->ulcmd[0] && chk_ar(cfg.prot[i]->ar,&useron,&client))
 				sprintf(keys+strlen(keys),"%c",cfg.prot[i]->mnemonic);
diff --git a/src/sbbs3/userdat.c b/src/sbbs3/userdat.c
index 98053a4bd97171817167604e304b32536db66256..ada3b2ded35b407dc6b006870540af20904f6438 100644
--- a/src/sbbs3/userdat.c
+++ b/src/sbbs3/userdat.c
@@ -31,15 +31,15 @@
 #include "smblib.h"
 #include "getstats.h"
 #include "msgdate.h"
+#include "scfglib.h"
 
 #ifndef USHRT_MAX
 	#define USHRT_MAX ((unsigned short)~0)
 #endif
 
 /* convenient space-saving global variables */
-char* crlf="\r\n";
-char* nulstr="";
-
+static const char* crlf="\r\n";
+static const char* nulstr="";
 static const char* strIpFilterExemptConfigFile = "ipfilter_exempt.cfg";
 
 #define VALID_CFG(cfg)	(cfg!=NULL && cfg->size==sizeof(scfg_t))
@@ -683,7 +683,7 @@ char* username(scfg_t* cfg, int usernumber, char *name)
 /****************************************************************************/
 /* Puts 'name' into slot 'number' in user/name.dat							*/
 /****************************************************************************/
-int putusername(scfg_t* cfg, int number, char *name)
+int putusername(scfg_t* cfg, int number, const char *name)
 {
 	char str[256];
 	int file;
@@ -1576,46 +1576,6 @@ int getnodeclient(scfg_t* cfg, uint number, client_t* client, time_t* done)
 	return sock;
 }
 
-static int getdirnum(scfg_t* cfg, char* code)
-{
-	size_t i;
-
-	for(i=0;i<cfg->total_dirs;i++)
-		if(stricmp(cfg->dir[i]->code,code)==0)
-			return(i);
-	return(-1);
-}
-
-static int getlibnum(scfg_t* cfg, char* code)
-{
-	size_t i;
-
-	for(i=0;i<cfg->total_dirs;i++)
-		if(stricmp(cfg->dir[i]->code,code)==0)
-			return(cfg->dir[i]->lib);
-	return(-1);
-}
-
-static int getsubnum(scfg_t* cfg, char* code)
-{
-	size_t i;
-
-	for(i=0;i<cfg->total_subs;i++)
-		if(stricmp(cfg->sub[i]->code,code)==0)
-			return(i);
-	return(-1);
-}
-
-static int getgrpnum(scfg_t* cfg, char* code)
-{
-	size_t i;
-
-	for(i=0;i<cfg->total_subs;i++)
-		if(stricmp(cfg->sub[i]->code,code)==0)
-			return(cfg->sub[i]->grp);
-	return(-1);
-}
-
 static BOOL ar_exp(scfg_t* cfg, uchar **ptrptr, user_t* user, client_t* client)
 {
 	BOOL	result,not,or,equal;
@@ -2456,51 +2416,51 @@ BOOL user_sent_email(scfg_t* cfg, user_t* user, int count, BOOL feedback)
 	return(TRUE);
 }
 
-BOOL user_downloaded(scfg_t* cfg, user_t* user, int files, long bytes)
+BOOL user_downloaded(scfg_t* cfg, user_t* user, int files, off_t bytes)
 {
 	if(user==NULL)
 		return(FALSE);
 
 	user->dls=(ushort)adjustuserrec(cfg, user->number, U_DLS, 5, files);
-	user->dlb=adjustuserrec(cfg, user->number, U_DLB, 10, bytes);
+	user->dlb=adjustuserrec(cfg, user->number, U_DLB, 10, (long)bytes);
 
 	return(TRUE);
 }
 
 #ifdef SBBS
 BOOL user_downloaded_file(scfg_t* cfg, user_t* user, client_t* client,
-	uint dirnum, const char* filename, ulong bytes)
+	uint dirnum, const char* filename, off_t bytes)
 {
-	file_t f = {{0}};
+	file_t f;
 
-	f.dir = dirnum;
-	padfname(getfname(filename), f.name);
-	if(!getfileixb(cfg, &f) || !getfiledat(cfg, &f))
+	if(!loadfile(cfg, dirnum, filename, &f, file_detail_normal))
 		return FALSE;
 
 	if(!bytes)
-		bytes = f.size;
+		bytes = getfilesize(cfg, &f);
 
-	f.timesdled++;
-	f.datedled=time32(NULL);
-	if(!putfiledat(cfg, &f) || !putfileixb(cfg, &f))
+	f.hdr.times_downloaded++;
+	f.hdr.last_downloaded = time32(NULL);
+	if(!updatefile(cfg, &f)) {
+		smb_freefilemem(&f);
 		return FALSE;
+	}
 
 	/**************************/
 	/* Update Uploader's Info */
 	/**************************/
 	user_t uploader = {0};
-	uploader.number=matchuser(cfg, f.uler, TRUE /*sysop_alias*/);
+	uploader.number=matchuser(cfg, f.from, TRUE /*sysop_alias*/);
 	if(uploader.number
 		&& uploader.number != user->number 
 		&& getuserdat(cfg, &uploader) == 0
-		&& uploader.firston < f.dateuled) {
-		ulong l = f.cdt;
-		if(!(cfg->dir[f.dir]->misc&DIR_CDTDL))	/* Don't give credits on d/l */
+		&& (uint32_t)uploader.firston < f.hdr.when_imported.time) {
+		ulong l = (ulong)f.cost;
+		if(!(cfg->dir[dirnum]->misc&DIR_CDTDL))	/* Don't give credits on d/l */
 			l=0;
-		ulong mod=(ulong)(l*(cfg->dir[f.dir]->dn_pct/100.0));
+		ulong mod=(ulong)(l*(cfg->dir[dirnum]->dn_pct/100.0));
 		adjustuserrec(cfg, uploader.number, U_CDT, 10, mod);
-		if(cfg->text != NULL && !(cfg->dir[f.dir]->misc&DIR_QUIET)) {
+		if(cfg->text != NULL && !(cfg->dir[dirnum]->misc&DIR_QUIET)) {
 			char str[256];
 			char tmp[128];
 			char prefix[128]="";
@@ -2513,7 +2473,7 @@ BOOL user_downloaded_file(scfg_t* cfg, user_t* user, client_t* client,
 					SAFEPRINTF2(username,"%s [%s]", user->alias, client->addr);
 			} else
 				SAFECOPY(username, user->alias);
-			if(strcmp(cfg->dir[f.dir]->code, "TEMP") == 0 || bytes < (ulong)f.size)
+			if(strcmp(cfg->dir[dirnum]->code, "TEMP") == 0 || bytes < (ulong)f.size)
 				SAFECOPY(prefix, cfg->text[Partially]);
 			if(client != NULL) {
 				SAFECAT(prefix, client->protocol);
@@ -2531,23 +2491,24 @@ BOOL user_downloaded_file(scfg_t* cfg, user_t* user, client_t* client,
 	/* Update Downloader's Info */
 	/****************************/
 	user_downloaded(cfg, user, /* files: */1, bytes);
-	if(!is_download_free(cfg, f.dir, user, client))
-		subtract_cdt(cfg, user, f.cdt);
+	if(!is_download_free(cfg, dirnum, user, client))
+		subtract_cdt(cfg, user, (long)f.cost);
 
-	if(!(cfg->dir[f.dir]->misc&DIR_NOSTAT))
-		inc_sys_download_stats(cfg, /* files: */1, bytes);
+	if(!(cfg->dir[dirnum]->misc&DIR_NOSTAT))
+		inc_sys_download_stats(cfg, /* files: */1, (ulong)bytes);
 
+	smb_freefilemem(&f);
 	return TRUE;
 }
 #endif
 
-BOOL user_uploaded(scfg_t* cfg, user_t* user, int files, long bytes)
+BOOL user_uploaded(scfg_t* cfg, user_t* user, int files, off_t bytes)
 {
 	if(user==NULL)
 		return(FALSE);
 
 	user->uls=(ushort)adjustuserrec(cfg, user->number, U_ULS, 5, files);
-	user->ulb=adjustuserrec(cfg, user->number, U_ULB, 10, bytes);
+	user->ulb=adjustuserrec(cfg, user->number, U_ULB, 10, (long)bytes);
 
 	return(TRUE);
 }
@@ -3172,7 +3133,7 @@ BOOL filter_ip(scfg_t* cfg, const char* prot, const char* reason, const char* ho
 	char	exempt[MAX_PATH+1];
 	char	tstr[64];
     FILE*	fp;
-    time32_t now=time32(NULL);
+    time_t	now = time(NULL);
 
 	if(ip_addr==NULL)
 		return(FALSE);
@@ -3196,7 +3157,7 @@ BOOL filter_ip(scfg_t* cfg, const char* prot, const char* reason, const char* ho
     fprintf(fp, "\n; %s %s ", prot, reason);
 	if(username != NULL)
 		fprintf(fp, "by %s ", username);
-    fprintf(fp,"on %s\n", timestr(cfg, now, tstr));
+    fprintf(fp,"on %.24s\n", ctime_r(&now, tstr));
 
 	if(host!=NULL)
 		fprintf(fp,"; Hostname: %s\n",host);
@@ -3599,6 +3560,14 @@ BOOL putmsgptrs(scfg_t* cfg, user_t* user, subscan_t* subscan)
 	return result;
 }
 
+BOOL newmsgs(smb_t* smb, time_t t)
+{
+	char index_fname[MAX_PATH + 1];
+
+	SAFEPRINTF(index_fname, "%s.sid", smb->file);
+	return fdate(index_fname) >= t;
+}
+
 /****************************************************************************/
 /* Initialize new-msg-scan pointers (e.g. for new users)					*/
 /* If 'days' is specified as 0, just set pointer to last message (faster)	*/
diff --git a/src/sbbs3/userdat.h b/src/sbbs3/userdat.h
index 4014af73a015c1d588a3bf4656625f1d6e6a4aca..2f47bfadbd4e32d46aeb31dbeec371e5a21073cc 100644
--- a/src/sbbs3/userdat.h
+++ b/src/sbbs3/userdat.h
@@ -33,9 +33,6 @@
 extern "C" {
 #endif
 
-extern char* crlf;
-extern char* nulstr;
-
 DLLEXPORT int	openuserdat(scfg_t*, BOOL for_modify);
 DLLEXPORT int	closeuserdat(int);
 DLLEXPORT int	readuserdat(scfg_t*, unsigned user_number, char* userdat, int infile);
@@ -47,7 +44,7 @@ DLLEXPORT int	newuserdat(scfg_t*, user_t*);	/* Create new userdat in user file *
 DLLEXPORT uint	matchuser(scfg_t*, const char *str, BOOL sysop_alias); /* Checks for a username match */
 DLLEXPORT BOOL	matchusername(scfg_t*, const char* name, const char* compare);
 DLLEXPORT char* alias(scfg_t*, const char* name, char* buf);
-DLLEXPORT int	putusername(scfg_t*, int number, char * name);
+DLLEXPORT int	putusername(scfg_t*, int number, const char* name);
 DLLEXPORT uint	total_users(scfg_t*);
 DLLEXPORT uint	lastuser(scfg_t*);
 DLLEXPORT BOOL	del_lastuser(scfg_t*);
@@ -110,19 +107,19 @@ DLLEXPORT BOOL	user_set_property(scfg_t*, unsigned user_number, const char* sect
 DLLEXPORT BOOL	user_set_time_property(scfg_t*, unsigned user_number, const char* section, const char* key, time_t);
 
 /* New-message-scan pointer functions: */
+DLLEXPORT BOOL	newmsgs(smb_t*, time_t);
 DLLEXPORT BOOL	getmsgptrs(scfg_t*, user_t*, subscan_t*, void (*progress)(void*, int, int), void* cbdata);
 DLLEXPORT BOOL	putmsgptrs(scfg_t*, user_t*, subscan_t*);
 DLLEXPORT BOOL	fixmsgptrs(scfg_t*, subscan_t*);
 DLLEXPORT BOOL	initmsgptrs(scfg_t*, subscan_t*, unsigned days, void (*progress)(void*, int, int), void* cbdata);
 
-
 /* New atomic numeric user field adjustment functions: */
 DLLEXPORT BOOL	user_posted_msg(scfg_t*, user_t*, int count);
 DLLEXPORT BOOL	user_sent_email(scfg_t*, user_t*, int count, BOOL feedback);
-DLLEXPORT BOOL	user_downloaded(scfg_t*, user_t*, int files, long bytes);
-DLLEXPORT BOOL	user_downloaded_file(scfg_t*, user_t*, client_t*, uint dirnum, const char* filename, ulong bytes);
+DLLEXPORT BOOL	user_downloaded(scfg_t*, user_t*, int files, off_t bytes);
+DLLEXPORT BOOL	user_downloaded_file(scfg_t*, user_t*, client_t*, uint dirnum, const char* filename, off_t bytes);
 
-DLLEXPORT BOOL	user_uploaded(scfg_t*, user_t*, int files, long bytes);
+DLLEXPORT BOOL	user_uploaded(scfg_t*, user_t*, int files, off_t bytes);
 DLLEXPORT BOOL	user_adjust_credits(scfg_t*, user_t*, long amount);
 DLLEXPORT BOOL	user_adjust_minutes(scfg_t*, user_t*, long amount);
 
diff --git a/src/sbbs3/useredit.cpp b/src/sbbs3/useredit.cpp
index 07f7176d0895558d3fd54c39765afe961614eec1..77a27c4d9f073a5b514fe65ce4f1e30955c4f487 100644
--- a/src/sbbs3/useredit.cpp
+++ b/src/sbbs3/useredit.cpp
@@ -1,7 +1,5 @@
 /* Synchronet online sysop user editor */
 
-/* $Id: useredit.cpp,v 1.75 2020/08/04 04:26:03 rswindell Exp $ */
-
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
  * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
@@ -15,21 +13,9 @@
  * See the GNU General Public License for more details: gpl.txt or			*
  * http://www.fsf.org/copyleft/gpl.html										*
  *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
- *																			*
  * For Synchronet coding style and modification guidelines, see				*
  * http://www.synchro.net/source.html										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
@@ -39,6 +25,7 @@
 
 #include "sbbs.h"
 #include "petdefs.h"
+#include "filedat.h"
 
 #define SEARCH_TXT 0
 #define SEARCH_ARS 1
@@ -1032,11 +1019,20 @@ void sbbs_t::maindflts(user_t* user)
 					putuserrec(&cfg,user->number,U_SHELL,8,cfg.shell[i]->code);
 				break;
 			case 'A':
-				for(i=0;i<cfg.total_fcomps;i++)
-					uselect(1,i,text[ArchiveTypeHeading],cfg.fcomp[i]->ext,cfg.fcomp[i]->ar);
+			{
+				str_list_t ext_list = strListDup((str_list_t)supported_archive_formats);
+				for(i=0; i < cfg.total_fcomps; i++) {
+					if(strListFind(ext_list, cfg.fcomp[i]->ext, /* case-sensitive */FALSE) < 0
+						&& chk_ar(cfg.fcomp[i]->ar, &useron, &client))
+						strListPush(&ext_list, cfg.fcomp[i]->ext);
+				}
+				for(i=0; ext_list[i] != NULL; i++)
+					uselect(1,i,text[ArchiveTypeHeading], ext_list[i], NULL);
 				if((i=uselect(0,0,0,0,0))>=0)
-					putuserrec(&cfg,user->number,U_TMPEXT,3,cfg.fcomp[i]->ext);
+					putuserrec(&cfg,user->number,U_TMPEXT,3,ext_list[i]);
+				strListFree(&ext_list);
 				break;
+			}
 			case 'L':
 				bputs(text[HowManyColumns]);
 				if((i = getnum(TERM_COLS_MAX)) < 0)
diff --git a/src/sbbs3/v4upgrade.c b/src/sbbs3/v4upgrade.c
index 05309ff23df908309227f371dbc979f1fa3b70ed..325ce68a67d08f3fe663a3fe9f55a1251fc9df6f 100644
--- a/src/sbbs3/v4upgrade.c
+++ b/src/sbbs3/v4upgrade.c
@@ -33,6 +33,7 @@
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
+#include <stdbool.h>
 #include "sbbs.h"
 #include "sbbs4defs.h"
 #include "ini_file.h"
@@ -112,6 +113,7 @@ BOOL upgrade_users(void)
 	int		ret;
 	size_t	len;
 	user_t	user;
+	int		userdat;
 
 	printf("Upgrading user database...     ");
 
@@ -123,15 +125,21 @@ BOOL upgrade_users(void)
 		return(FALSE);
 	}
 
+	if((userdat = openuserdat(&scfg, /* for_modify: */FALSE)) < 0) {
+		perror("user.dat");
+		return FALSE;
+	}
+
 	fprintf(out,"%-*.*s\r\n",USER_REC_LEN,USER_REC_LEN,tabLineCreator(user_dat_columns));
 
 	total=lastuser(&scfg);
 	for(i=1;i<=total;i++) {
 		printf("\b\b\b\b\b%5u",total-i);
 		memset(&user,0,sizeof(user));
-		user.number=i;
-		if((ret=getuserdat(&scfg,&user))!=0) {
+		user.number = i;
+		if((ret=fgetuserdat(&scfg, &user, userdat))!=0) {
 			printf("\nError %d reading user.dat\n",ret);
+			closeuserdat(userdat);
 			return(FALSE);
 		}
 		/******************************************/
@@ -244,7 +252,7 @@ BOOL upgrade_users(void)
 		len+=sprintf(rec+len,"%u\t%c\t%s\t%s\t%s\t%s\t%s\t%s\t"
 			,user.rows
 			,user.prot
-			,user.xedit ? scfg.xedit[user.xedit]->code : ""
+			,user.xedit && user.xedit <= scfg.total_xedits ? scfg.xedit[user.xedit-1]->code : ""
 			,scfg.shell[user.shell]->code
 			,user.tmpext
 			,user.cursub
@@ -256,10 +264,12 @@ BOOL upgrade_users(void)
 		if((ret=fprintf(out,"%-*.*s\r\n",USER_REC_LEN,USER_REC_LEN,rec))!=USER_REC_LINE_LEN) {
 			printf("!Error %d (errno: %d) writing %u bytes to user.tab\n"
 				,ret, errno, USER_REC_LINE_LEN);
+			closeuserdat(userdat);
 			return(FALSE);
 		}
 	}
 	fclose(out);
+	closeuserdat(userdat);
 
 	printf("\n\tdata/user/user.dat -> %s (%u users)\n", outpath,total);
 
@@ -267,29 +277,29 @@ BOOL upgrade_users(void)
 }
 
 typedef struct {
-	time_t	time;
-	ulong	ltoday;
-	ulong	ttoday;
-	ulong	uls;
-	ulong	ulb;
-	ulong	dls;
-	ulong	dlb;
-	ulong	ptoday;
-	ulong	etoday;
-	ulong	ftoday;
+	time32_t	time;
+	uint32_t	ltoday;
+	uint32_t	ttoday;
+	uint32_t	uls;
+	uint32_t	ulb;
+	uint32_t	dls;
+	uint32_t	dlb;
+	uint32_t	ptoday;
+	uint32_t	etoday;
+	uint32_t	ftoday;
 } csts_t;
 
 BOOL upgrade_stats(void)
 {
-	char	inpath[MAX_PATH+1];
-	char	outpath[MAX_PATH+1];
-	BOOL	success;
-	ulong	count;
+	char		inpath[MAX_PATH+1];
+	char		outpath[MAX_PATH+1];
+	BOOL		success;
+	ulong		count;
 	time32_t	t;
-	stats_t	stats;
-	FILE*	in;
-	FILE*	out;
-	csts_t	csts;
+	stats_t		stats;
+	FILE*		in;
+	FILE*		out;
+	csts_t		csts;
 	str_list_t	list;
 
 	printf("Upgrading statistics data...\n");
@@ -317,7 +327,7 @@ BOOL upgrade_stats(void)
 		return(FALSE);
 	}
 
-	iniSetDateTime(&list,	ROOT_SECTION	,"TimeStamp"	,TRUE, t		,NULL);
+	iniSetDateTime(&list,	ROOT_SECTION	,"TimeStamp"	,/* include time: */TRUE, t, NULL);
 	iniSetInteger(&list,	ROOT_SECTION	,"Logons"		,stats.logons	,NULL);
 	iniSetInteger(&list,	ROOT_SECTION	,"LogonsToday"	,stats.ltoday	,NULL);
 	iniSetInteger(&list,	ROOT_SECTION	,"Timeon"		,stats.timeon	,NULL);
@@ -424,11 +434,12 @@ BOOL upgrade_event_data(void)
 	for(i=0;i<scfg.total_events;i++) {
 		t=0;
 		fread(&t,1,sizeof(t),in);
-		iniSetHexInt(&list, "Events", scfg.event[i]->code, t, NULL);
+		iniSetDateTime(&list, "Events", scfg.event[i]->code, /* include time: */TRUE, t, NULL);
 	}
 	t=0;
 	fread(&t,1,sizeof(t),in);
-	iniSetHexInt(&list,ROOT_SECTION,"QWKPrePack",t,NULL);
+	if(t != 0)
+		iniSetDateTime(&list,ROOT_SECTION,"QWKPrePack", /* include time: */TRUE,t,NULL);
 	fclose(in);
 
 	printf("-> %s (%u timed events)\n", outpath, i);
@@ -443,7 +454,7 @@ BOOL upgrade_event_data(void)
 		for(i=0;i<scfg.total_qhubs;i++) {
 			t=0;
 			fread(&t,1,sizeof(t),in);
-			iniSetHexInt(&list,"QWKNetworkHubs",scfg.qhub[i]->id,t,NULL);
+			iniSetDateTime(&list,"QWKNetworkHubs",scfg.qhub[i]->id, /* include time: */TRUE,t,NULL);
 		}
 		fclose(in);
 	}
@@ -931,107 +942,13 @@ BOOL upgrade_ftp_aliases(void)
 	return(success);
 }
 
-BOOL upgrade_socket_options(void)
-{
-	char*	p;
-	char*	key;
-	char*	val;
-	char*	section;
-	char	inpath[MAX_PATH+1];
-	char	outpath[MAX_PATH+1];
-	FILE*	in;
-	FILE*	out;
-	BOOL	success;
-	size_t	i;
-	size_t	total;
-	str_list_t	inlist;
-	str_list_t	outlist;
-
-	style.section_separator = "";
-	iniSetDefaultStyle(style);
-
-	SAFEPRINTF(inpath,"%ssockopts.cfg",scfg.ctrl_dir);
-	SAFEPRINTF(outpath,"%ssockopts.ini",scfg.ctrl_dir);
-
-	if(!fexistcase(inpath))
-		return(TRUE);
-
-	printf("Upgrading Socket Options...\n");
-
-	if(!overwrite(outpath))
-		return(TRUE);
-	if((out=fopen(outpath,"w"))==NULL) {
-		perror(outpath);
-		return(FALSE);
-	}
-
-	if((outlist = strListInit())==NULL) {
-		printf("!malloc failure\n");
-		return(FALSE);
-	}
-	printf("\t%s ",inpath);
-	if((in=fopen(inpath,"r"))==NULL) {
-		perror("open failure");
-		return(FALSE);
-	}
-
-	if((inlist = strListReadFile(in,NULL,4096))==NULL) {
-		printf("!failure reading %s\n",inpath);
-		return(FALSE);
-	}
-
-	total=0;
-	for(i=0;inlist[i]!=NULL;i++) {
-		p=truncsp(inlist[i]);
-		SKIP_WHITESPACE(p);
-		if(*p==';') {
-			strListPush(&outlist,p);
-			continue;
-		} else if(*p==0)
-			continue;
-		key=p;
-		FIND_WHITESPACE(p);
-		if(*p==0)
-			continue;
-		*(p++)=0;
-		SKIP_WHITESPACE(p);
-		val=p;
-		section=ROOT_SECTION;
-		if(!stricmp(key,"tcp_nodelay"))
-			section="telnet|rlogin";
-		else if(!stricmp(key,"keepalive"))
-			section="tcp";
-		iniSetString(&outlist,section,key,val,NULL);
-		total++;
-	}
-
-	printf("-> %s (%u Socket Options)\n", outpath, total);
-	fclose(in);
-	strListFree(&inlist);
-
-	success=iniWriteFile(out, outlist);
-
-	fclose(out);
-
-	if(!success) {
-		printf("!iniWriteFile failure\n");
-		return(FALSE);
-	}
-
-	strListFree(&outlist);
-
-	return(success);
-}
-
-
-
 #define upg_iniSetString(list,section,key,val) \
 		if(*val) iniSetString(list,section,key,val,NULL)
 
 #define upg_iniSetInteger(list,section,key,val) \
 		if(val) iniSetInteger(list,section,key,val,NULL)
 
-BOOL upgrade_msg_areas(void)
+BOOL upgrade_msg_area_cfg(void)
 {
 	char	str[128];
 	char	outpath[MAX_PATH+1];
@@ -1071,9 +988,8 @@ BOOL upgrade_msg_areas(void)
 		upg_iniSetString(&outlist,str,"CodePrefix",scfg.grp[i]->code_prefix);
 	}
 	for(i=0; i<scfg.total_subs; i++) {
-		sprintf(str,"%s",scfg.sub[i]->code_suffix);
+		sprintf(str,"Sub:%s:%s", scfg.grp[scfg.sub[i]->grp]->sname, scfg.sub[i]->code_suffix);
 		iniAppendSection(&outlist,str,NULL);
-		upg_iniSetString(&outlist,str,"Group",scfg.grp[scfg.sub[i]->grp]->sname);
 		upg_iniSetString(&outlist,str,"Name",scfg.sub[i]->sname);
 		upg_iniSetString(&outlist,str,"Newsgroup",scfg.sub[i]->newsgroup);
 		upg_iniSetString(&outlist,str,"QwkName",scfg.sub[i]->qwkname);
@@ -1115,6 +1031,136 @@ BOOL upgrade_msg_areas(void)
 	return(success);
 }
 
+int file_uldate_compare(const void* v1, const void* v2)
+{
+	file_t* f1 = (file_t*)v1;
+	file_t* f2 = (file_t*)v2;
+
+	return f1->dateuled - f2->dateuled;
+}
+
+bool upgrade_file_areas(void)
+{
+	int result;
+	ulong total_files = 0;
+	time_t start = time(NULL);
+
+	printf("Upgrading File areas...\n");
+
+	for(int i = 0; i < scfg.total_dirs; i++) {
+		smb_t smb;
+
+		SAFEPRINTF2(smb.file, "%s%s", scfg.dir[i]->data_dir, scfg.dir[i]->code);
+		if((result = smb_open(&smb)) != SMB_SUCCESS) {
+			fprintf(stderr, "Error %d (%s) opening %s\n", result, smb.last_error, smb.file);
+			return false;
+		}
+		smb.status.attr = SMB_FILE_DIRECTORY|SMB_NOHASH;
+		smb.status.max_age = scfg.dir[i]->maxage;
+		smb.status.max_msgs = scfg.dir[i]->maxfiles;
+		if((result = smb_create(&smb)) != SMB_SUCCESS)
+			return false;
+
+		char str[MAX_PATH+1];
+		int file;
+		int extfile = openextdesc(&scfg, i);
+
+		sprintf(str,"%s%s.ixb",scfg.dir[i]->data_dir,scfg.dir[i]->code);
+		if((file=open(str,O_RDONLY|O_BINARY))==-1)
+			continue;
+		long l=filelength(file);
+		if(!l) {
+			close(file);
+			continue;
+		}
+		uchar* ixbbuf;
+		if((ixbbuf=(uchar *)malloc(l))==NULL) {
+			close(file);
+			printf("\7ERR_ALLOC %s %lu\n",str,l);
+			continue;
+		}
+		if(read(file,ixbbuf,l)!=(int)l) {
+			close(file);
+			printf("\7ERR_READ %s %lu\n",str,l);
+			free(ixbbuf);
+			continue;
+		}
+		close(file);
+		size_t file_count = l / F_IXBSIZE;
+		file_t* filelist = malloc(sizeof(file_t) * file_count);
+		memset(filelist, 0, sizeof(file_t) * file_count);
+		file_t* f = filelist;
+		long m=0L;
+		while(m<l) {
+			int j;
+			f->dir = i;
+			for(j=0;j<12 && m<l;j++)
+				if(j==8)
+					f->name[j]=ixbbuf[m]>' ' ? '.' : ' ';
+				else
+					f->name[j]=ixbbuf[m++]; /* Turns FILENAMEEXT into FILENAME.EXT */
+			f->name[j]=0;
+			f->datoffset=ixbbuf[m]|((long)ixbbuf[m+1]<<8)|((long)ixbbuf[m+2]<<16);
+			f->dateuled=(ixbbuf[m+3]|((long)ixbbuf[m+4]<<8)|((long)ixbbuf[m+5]<<16)
+				|((long)ixbbuf[m+6]<<24));
+			f->datedled =(ixbbuf[m+7]|((long)ixbbuf[m+8]<<8)|((long)ixbbuf[m+9]<<16)
+				|((long)ixbbuf[m+10]<<24));
+			m+=11;
+			f++;
+		};
+
+		/* SMB index is sorted by import (upload) time */
+		qsort(filelist, file_count, sizeof(*filelist), file_uldate_compare);
+
+		for(size_t fi = 0; fi < file_count; fi++) {
+			f = &filelist[fi];
+			if(!getfiledat(&scfg, f)) {
+				fprintf(stderr, "Error getting file data for %s %s\n", scfg.dir[i]->code, f->name);
+				continue;
+			}
+			char fpath[MAX_PATH+1];
+			getfilepath(&scfg, f, fpath);
+			smbfile_t file;
+			memset(&file, 0, sizeof(file));
+			file.hdr.when_written.time = (time32_t)fdate(fpath);
+			file.hdr.when_imported.time = f->dateuled;
+			file.hdr.last_downloaded = f->datedled;
+			file.hdr.times_downloaded = f->timesdled;
+			file.hdr.altpath = f->altpath;
+			smb_hfield_str(&file, SMB_FILENAME, getfname(fpath));
+			smb_hfield_str(&file, SMB_FILEDESC, f->desc);
+			smb_hfield_str(&file, SENDER, f->uler);
+			smb_hfield_bin(&file, SMB_COST, f->cdt);
+			if(f->misc&FM_ANON)
+				file.hdr.attr |= MSG_ANONYMOUS;
+			{
+				const char* body = NULL;
+				char extdesc[F_EXBSIZE+1] = {0};
+				if(f->misc&FM_EXTDESC) {
+					fgetextdesc(&scfg, i, f->datoffset, extdesc, extfile);
+					truncsp(extdesc);
+					body = extdesc;
+				}
+				result = smb_addfile(&smb, &file, SMB_FASTALLOC, body);
+			}
+			if(result != SMB_SUCCESS) {
+				fprintf(stderr, "Error %d (%s) adding file to %s\n", result, smb.last_error, smb.file);
+			} else {
+				total_files++;
+				time_t diff = time(NULL) - start;
+				printf("\r%-16s (%-5u areas remain) %u files imported (%u files/second)"
+					, scfg.dir[i]->code, scfg.total_dirs - (i + 1), total_files, diff ? total_files / diff : total_files);
+			}
+		}
+		free(filelist);
+		smb_close(&smb);
+		closeextdesc(extfile);
+		free(ixbbuf);
+	}
+	printf("\r%u files imported in %u directories%40s\n", total_files, scfg.total_dirs,"");
+
+	return true;
+}
 
 char *usage="\nusage: v4upgrade [ctrl_dir]\n";
 
@@ -1139,7 +1185,7 @@ int main(int argc, char** argv)
 	if(p==NULL) {
 		printf("\nSBBSCTRL environment variable not set.\n");
 		printf("\nExample: SET SBBSCTRL=/sbbs/ctrl\n");
-		exit(1); 
+		return EXIT_FAILURE + __COUNTER__;
 	}
 
 	memset(&scfg,0,sizeof(scfg));
@@ -1152,61 +1198,63 @@ int main(int argc, char** argv)
 	printf("\nLoading configuration files from %s\n",scfg.ctrl_dir);
 	if(!load_cfg(&scfg,NULL,TRUE,error)) {
 		fprintf(stderr,"!ERROR loading configuration files: %s\n",error);
-		exit(1);
+		return EXIT_FAILURE + __COUNTER__;
 	}
 
 	iniSetDefaultStyle(style);
 
 	if(!upgrade_users())
-		return(1);
+		return EXIT_FAILURE + __COUNTER__;
 
 	if(!upgrade_stats())
-		return(2);
+		return EXIT_FAILURE + __COUNTER__;
 
 	if(!upgrade_event_data())
-		return(3);
+		return EXIT_FAILURE + __COUNTER__;
 
 	if(!upgrade_filters())
-		return(4);
+		return EXIT_FAILURE + __COUNTER__;
 
 	if(!upgrade_list("Twits", "twitlist.cfg", "twitlist.ini", TRUE, NULL))
-		return(5);
+		return EXIT_FAILURE + __COUNTER__;
 
 	if(!upgrade_list("RLogin allow", "rlogin.cfg", "rlogin.ini", TRUE, NULL))
-		return(5);
+		return EXIT_FAILURE + __COUNTER__;
 
 	if(!upgrade_list("E-Mail aliases", "alias.cfg", "alias.ini", FALSE, NULL))
-		return(5);
+		return EXIT_FAILURE + __COUNTER__;
 	
 	if(!upgrade_list("E-Mail domains", "domains.cfg", "domains.ini", TRUE, NULL))
-		return(5);
+		return EXIT_FAILURE + __COUNTER__;
 
 	if(!upgrade_list("Allowed mail relayers", "relay.cfg", "relay.ini", TRUE, NULL))
-		return(5);
+		return EXIT_FAILURE + __COUNTER__;
 
 	if(!upgrade_list("SPAM bait addresses", "spambait.cfg", "spambait.ini", TRUE, NULL))
-		return(5);
+		return EXIT_FAILURE + __COUNTER__;
 
+#if 0 /* temporary disable for testing purposes only */
 	if(!upgrade_list("Blocked spammers", "spamblock.cfg", "spamblock.ini", TRUE, NULL))
-		return(5);
+		return EXIT_FAILURE + __COUNTER__;
+#endif
 
 	if(!upgrade_list("DNS black-lists", "dns_blacklist.cfg", "dns_blacklist.ini", TRUE, "notice"))
-		return(5);
+		return EXIT_FAILURE + __COUNTER__;
 
 	if(!upgrade_list("DNS black-list exemptions", "dnsbl_exempt.cfg", "dnsbl_exempt.ini", TRUE, NULL))
-		return(5);
+		return EXIT_FAILURE + __COUNTER__;
 
 	if(!upgrade_ftp_aliases())
-		return(-1);
-
-	if(!upgrade_socket_options())
-		return(-1);
+		return EXIT_FAILURE + __COUNTER__;
 
 	/* attr.cfg */
 
-	if(!upgrade_msg_areas())
-		return(-1);
+	if(!upgrade_msg_area_cfg())
+		return EXIT_FAILURE + __COUNTER__;
+
+	if(!upgrade_file_areas())
+		return EXIT_FAILURE + __COUNTER__;
 
 	printf("Upgrade successful.\n");
-    return(0);
+    return EXIT_SUCCESS;
 }
diff --git a/src/sbbs3/viewfile.cpp b/src/sbbs3/viewfile.cpp
index 79f930b824968fec585d349cdf078c10a0f3e9b0..b76ef3ff4df0cac76ff55974450676982fa86f52 100644
--- a/src/sbbs3/viewfile.cpp
+++ b/src/sbbs3/viewfile.cpp
@@ -1,9 +1,5 @@
-/* viewfile.cpp */
-
 /* Synchronet file contents display routines */
 
-/* $Id: viewfile.cpp,v 1.12 2020/05/11 08:57:19 rswindell Exp $ */
-
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
  * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
@@ -17,47 +13,38 @@
  * See the GNU General Public License for more details: gpl.txt or			*
  * http://www.fsf.org/copyleft/gpl.html										*
  *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
- *																			*
  * For Synchronet coding style and modification guidelines, see				*
  * http://www.synchro.net/source.html										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
 #include "sbbs.h"
+#include "filedat.h"
 
 /****************************************************************************/
 /* Views file with:                                                         */
 /* (B)atch download, (V)iew file (E)xtended info, (Q)uit, or [Next]:        */
 /* call with ext=1 for default to extended info, or 0 for file view         */
-/* Returns -1 for Batch, 1 for Next, or 0 for Quit                          */
+/* Returns -1 for Batch, 1 for Next, -2 for Previous, or 0 for Quit         */
 /****************************************************************************/
-int sbbs_t::viewfile(file_t* f, int ext)
+int sbbs_t::viewfile(file_t* f, bool ext)
 {
 	char	ch,str[256];
-	char 	tmp[512];
+	char	fname[13];	/* This is one of the only 8.3 filename formats left! (used for display purposes only) */
+	format_filename(f->name, fname, sizeof(fname)-1, /* pad: */FALSE);
 
 	curdirnum=f->dir;	/* for ARS */
 	while(online) {
+		sys_status &= ~SS_ABORT;
 		if(ext)
-			fileinfo(f);
+			showfileinfo(f);
 		else
 			viewfilecontents(f);
 		ASYNC;
-		sprintf(str,text[FileInfoPrompt],unpadfname(f->name,tmp));
+		SAFEPRINTF(str, text[FileInfoPrompt], fname);
 		mnemonics(str);
-		ch=(char)getkeys("BEVQN\r",0);
+		ch=(char)getkeys("BEVQPN\b-\r",0);
 		if(ch=='Q' || sys_status&SS_ABORT)
 			return(0);
 		switch(ch) {
@@ -71,6 +58,10 @@ int sbbs_t::viewfile(file_t* f, int ext)
 			case 'V':
 				ext=0;
 				continue;
+			case 'P':
+			case '\b':
+			case '-':
+				return -2;
 			case 'N':
 			case CR:
 				return(1); 
@@ -85,28 +76,31 @@ int sbbs_t::viewfile(file_t* f, int ext)
 /*****************************************************************************/
 void sbbs_t::viewfiles(uint dirnum, char *fspec)
 {
+	char	tmp[512];
     char	viewcmd[256];
-	char 	tmp[512];
     int		i;
 
 	curdirnum=dirnum;	/* for ARS */
-	sprintf(viewcmd,"%s%s",cfg.dir[dirnum]->path,fspec);
+	SAFEPRINTF2(viewcmd,"%s%s",cfg.dir[dirnum]->path,fspec);
 	if(!fexist(viewcmd)) {
 		bputs(text[FileNotFound]);
 		return; 
 	}
-	padfname(fspec,tmp);
-	truncsp(tmp);
+	char* file_ext = getfext(fspec);
+	if(file_ext == NULL) {
+		bprintf(text[NonviewableFile], fspec);
+		return; 
+	}
 	for(i=0;i<cfg.total_fviews;i++)
-		if(!stricmp(tmp+9,cfg.fview[i]->ext) && chk_ar(cfg.fview[i]->ar,&useron,&client)) {
-			strcpy(viewcmd,cfg.fview[i]->cmd);
+		if(!stricmp(file_ext + 1, cfg.fview[i]->ext) && chk_ar(cfg.fview[i]->ar,&useron,&client)) {
+			SAFECOPY(viewcmd,cfg.fview[i]->cmd);
 			break; 
 		}
 	if(i==cfg.total_fviews) {
-		bprintf(text[NonviewableFile],tmp+9);
+		bprintf(text[NonviewableFile], file_ext);
 		return; 
 	}
-	sprintf(tmp,"%s%s",cfg.dir[dirnum]->path,fspec);
+	SAFEPRINTF2(tmp, "%s%s", cfg.dir[dirnum]->path, fspec);
 	if((i=external(cmdstr(viewcmd,tmp,tmp,NULL),EX_STDIO|EX_SH))!=0)
 		errormsg(WHERE,ERR_EXEC,viewcmd,i);    /* must have EX_SH to ^C */
 }
@@ -121,9 +115,8 @@ void sbbs_t::viewfilecontents(file_t* f)
 	int		i;
 
 	getfilepath(&cfg, f, path);
-
-	if(f->size<=0L) {
-		bprintf(text[FileDoesNotExist],path);
+	if(getfilesize(&cfg, f) < 1) {
+		bprintf(text[FileDoesNotExist], path);
 		return; 
 	}
 	if((ext=getfext(path))!=NULL) {
@@ -131,7 +124,7 @@ void sbbs_t::viewfilecontents(file_t* f)
 		for(i=0;i<cfg.total_fviews;i++) {
 			if(!stricmp(ext,cfg.fview[i]->ext)
 				&& chk_ar(cfg.fview[i]->ar,&useron,&client)) {
-				strcpy(cmd,cfg.fview[i]->cmd);
+				SAFECOPY(cmd,cfg.fview[i]->cmd);
 				break; 
 			} 
 		}
diff --git a/src/sbbs3/websrvr.c b/src/sbbs3/websrvr.c
index bc52448bc9999f601469085e3eb3da3cf98d298c..2329ab854dd04026e7b2bca9f1faf0b508367281 100644
--- a/src/sbbs3/websrvr.c
+++ b/src/sbbs3/websrvr.c
@@ -65,7 +65,8 @@
 #include "xpprintf.h"
 #include "ssl.h"
 #include "fastcgi.h"
-#include "ver.h"
+#include "git_branch.h"
+#include "git_hash.h"
 
 static const char*	server_name="Synchronet Web Server";
 static const char*	newline="\r\n";
@@ -212,8 +213,8 @@ typedef struct  {
 	BOOL		finished;				/* Done processing request. */
 	BOOL		read_chunked;
 	BOOL		write_chunked;
-	long		range_start;
-	long		range_end;
+	off_t		range_start;
+	off_t		range_end;
 	BOOL		accept_ranges;
 	time_t		if_range;
 	BOOL		path_info_index;
@@ -1430,13 +1431,13 @@ static BOOL send_headers(http_session_t *session, const char *status, int chunke
 	return (ret);
 }
 
-static off_t sock_sendfile(http_session_t *session,char *path,unsigned long start, unsigned long end)
+static off_t sock_sendfile(http_session_t *session,char *path, off_t start, off_t end)
 {
 	int		file;
 	off_t	ret=0;
 	ssize_t	i;
 	char	buf[OUTBUF_LEN];		/* Input buffer */
-	unsigned long		remain;
+	uint64_t remain;
 
 	if(startup->options&WEB_OPT_DEBUG_TX)
 		lprintf(LOG_DEBUG,"%04d Sending %s",session->socket,path);
@@ -1454,7 +1455,7 @@ static off_t sock_sendfile(http_session_t *session,char *path,unsigned long star
 		else {
 			remain=-1L;
 		}
-		while((i=read(file, buf, remain>sizeof(buf)?sizeof(buf):remain))>0) {
+		while((i=read(file, buf, (size_t)(remain>sizeof(buf)?sizeof(buf):remain)))>0) {
 			if(writebuf(session,buf,i)!=i) {
 				lprintf(LOG_WARNING,"%04d !ERROR sending %s",session->socket,path);
 				close(file);
@@ -1737,7 +1738,7 @@ static BOOL digest_authentication(http_session_t* session, int auth_allowed, use
 	MD5_digest(&ctx, ":", 1);
 	MD5_digest(&ctx, thisuser.pass, strlen(thisuser.pass));
 	MD5_close(&ctx, digest);
-	MD5_hex((BYTE*)ha1, digest);
+	MD5_hex(ha1, digest);
 
 	/* H(A1)l */
 	pass=strdup(thisuser.pass);
@@ -1749,7 +1750,7 @@ static BOOL digest_authentication(http_session_t* session, int auth_allowed, use
 	MD5_digest(&ctx, ":", 1);
 	MD5_digest(&ctx, pass, strlen(pass));
 	MD5_close(&ctx, digest);
-	MD5_hex((BYTE*)ha1l, digest);
+	MD5_hex(ha1l, digest);
 
 	/* H(A1)u */
 	strupr(pass);
@@ -1760,7 +1761,7 @@ static BOOL digest_authentication(http_session_t* session, int auth_allowed, use
 	MD5_digest(&ctx, ":", 1);
 	MD5_digest(&ctx, thisuser.pass, strlen(thisuser.pass));
 	MD5_close(&ctx, digest);
-	MD5_hex((BYTE*)ha1u, digest);
+	MD5_hex(ha1u, digest);
 	free(pass);
 
 	/* H(A2) */
@@ -1775,7 +1776,7 @@ static BOOL digest_authentication(http_session_t* session, int auth_allowed, use
 		return(FALSE);
 	}
 	MD5_close(&ctx, digest);
-	MD5_hex((BYTE*)ha2, digest);
+	MD5_hex(ha2, digest);
 
 	/* Check password as in user.dat */
 	calculate_digest(session, ha1, ha2, digest);
@@ -3624,7 +3625,7 @@ static BOOL check_request(http_session_t * session)
 						for(sp=spath, nsp=find_first_slash(sp+1); nsp; nsp=find_first_slash(sp+1)) {
 							*nsp=0;
 							nsp++;
-							if(wildmatch(sp, pspec, TRUE)) {
+							if(wildmatch(sp, pspec, TRUE, /* case_sensitive: */TRUE)) {
 								read_webctrl_section(file, spec, session, curdir, &recheck_dynamic);
 							}
 							sp=nsp;
@@ -3632,7 +3633,7 @@ static BOOL check_request(http_session_t * session)
 						free(spath);
 						free(pspec);
 					}
-					else if(wildmatch(filename,spec,TRUE)) {
+					else if(wildmatch(filename,spec,TRUE, /* case_sensitive: */TRUE)) {
 						read_webctrl_section(file, spec, session, curdir, &recheck_dynamic);
 					}
 					free(spec);
@@ -6702,7 +6703,7 @@ const char* DLLCALL web_ver(void)
 #else
 		,""
 #endif
-		,git_branch, git_hash
+		,GIT_BRANCH, GIT_HASH
 		,__DATE__, __TIME__, compiler);
 
 	return(ver);
@@ -6922,7 +6923,7 @@ void DLLCALL web_server(void* arg)
 
 		DESCRIBE_COMPILER(compiler);
 
-		lprintf(LOG_INFO,"Compiled %s/%s %s %s with %s", git_branch, git_hash, __DATE__, __TIME__, compiler);
+		lprintf(LOG_INFO,"Compiled %s/%s %s %s with %s", GIT_BRANCH, GIT_HASH, __DATE__, __TIME__, compiler);
 
 		if(!winsock_startup()) {
 			cleanup(1);
diff --git a/src/sbbs3/websrvr.vcxproj b/src/sbbs3/websrvr.vcxproj
index 94519cae70b333f6d7750821c019c15abdefb1f2..931bfdd3b13b51df583a0edf8598369faf04749b 100644
--- a/src/sbbs3/websrvr.vcxproj
+++ b/src/sbbs3/websrvr.vcxproj
@@ -176,7 +176,6 @@
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
     </ClCompile>
-    <ClCompile Include="ver.cpp" />
     <ClCompile Include="websrvr.c">
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
diff --git a/src/sbbs3/writemsg.cpp b/src/sbbs3/writemsg.cpp
index 1cd3598eb7a1cc85636c66648089f80cb4ad0dfa..b103380539c7ac627821b6305d9ef528c731c102 100644
--- a/src/sbbs3/writemsg.cpp
+++ b/src/sbbs3/writemsg.cpp
@@ -1111,7 +1111,7 @@ ulong sbbs_t::msgeditor(char *buf, const char *top, char *title)
 				break;
 			}
 			else if(!stricmp(strin,"/T")) { /* Edit title/subject */
-				if(title[0]) {
+				if(title != nulstr) { // hack
 					bputs(text[SubjectPrompt]);
 					getstr(title,LEN_TITLE,K_LINE|K_EDIT|K_AUTODEL|K_TRIM);
 					SYNC;
@@ -1283,7 +1283,7 @@ bool sbbs_t::editfile(char *fname, bool msg)
 		buf[0]=0;
 		bputs(text[NewFile]); 
 	}
-	if(!msgeditor(buf,nulstr,nulstr)) {
+	if(!msgeditor(buf,nulstr,/* title: */(char*)nulstr)) {
 		free(buf);
 		return false; 
 	}
@@ -1605,7 +1605,8 @@ bool sbbs_t::editmsg(smb_t* smb, smbmsg_t *msg)
 	char	msgtmp[MAX_PATH+1];
 	uint16_t	xlat;
 	int 	file,i,j,x;
-	long	length,offset;
+	long	length;
+	off_t	offset;
 	FILE	*instream;
 
 	if(!msg->hdr.total_dfields)
@@ -1662,7 +1663,7 @@ bool sbbs_t::editmsg(smb_t* smb, smbmsg_t *msg)
 		smb_close_da(smb);
 	}
 
-	msg->hdr.offset=offset;
+	msg->hdr.offset=(uint32_t)offset;
 	if((file=open(msgtmp,O_RDONLY|O_BINARY))==-1
 		|| (instream=fdopen(file,"rb"))==NULL) {
 		smb_unlocksmbhdr(smb);
@@ -1672,7 +1673,7 @@ bool sbbs_t::editmsg(smb_t* smb, smbmsg_t *msg)
 	}
 
 	setvbuf(instream,NULL,_IOFBF,2*1024);
-	fseek(smb->sdt_fp,offset,SEEK_SET);
+	fseeko(smb->sdt_fp,offset,SEEK_SET);
 	xlat=XLAT_NONE;
 	fwrite(&xlat,2,1,smb->sdt_fp);
 	x=SDT_BLOCK_LEN-2;				/* Don't read/write more than 255 */
@@ -1704,7 +1705,8 @@ bool sbbs_t::movemsg(smbmsg_t* msg, uint subnum)
 	char str[256],*buf;
 	uint i;
 	int newgrp,newsub,storage;
-	ulong offset,length;
+	off_t offset;
+	ulong length;
 	smbmsg_t	newmsg=*msg;
 	smb_t		newsmb;
 
@@ -1786,10 +1788,10 @@ bool sbbs_t::movemsg(smbmsg_t* msg, uint subnum)
 		smb_close_da(&newsmb); 
 	}
 
-	newmsg.hdr.offset=offset;
+	newmsg.hdr.offset=(uint32_t)offset;
 	newmsg.hdr.version=smb_ver();
 
-	fseek(newsmb.sdt_fp,offset,SEEK_SET);
+	fseeko(newsmb.sdt_fp,offset,SEEK_SET);
 	fwrite(buf,length,1,newsmb.sdt_fp);
 	fflush(newsmb.sdt_fp);
 	free(buf);
diff --git a/src/sbbs3/xtrn.cpp b/src/sbbs3/xtrn.cpp
index 885b498936f212a7ad7d03aad757a5ed22b4eb96..457809718b3fc7df3cdacf604f35e681df0bc8e4 100644
--- a/src/sbbs3/xtrn.cpp
+++ b/src/sbbs3/xtrn.cpp
@@ -582,14 +582,11 @@ int sbbs_t::external(const char* cmdline, long mode, const char* startup_dir)
 		startup_info.dwFlags|=STARTF_USESTDHANDLES|STARTF_USESHOWWINDOW;
     	startup_info.wShowWindow=SW_HIDE;
 	}
-	if(native && !(mode&EX_OFFLINE)) {
-
-		if(!(mode&EX_STDIN)) {
-			if(passthru_thread_running)
-				passthru_socket_activate(true);
-			else
-				pthread_mutex_lock(&input_thread_mutex);
-		}
+	if(native && !(mode & (EX_OFFLINE | EX_STDIN))) {
+		if(passthru_thread_running)
+			passthru_socket_activate(true);
+		else
+			pthread_mutex_lock(&input_thread_mutex);
 	}
 
     success=CreateProcess(
@@ -609,7 +606,7 @@ int sbbs_t::external(const char* cmdline, long mode, const char* startup_dir)
 
 	if(!success) {
 		XTRN_CLEANUP;
-		if(!(mode&EX_STDIN)) {
+		if(native && !(mode & (EX_OFFLINE | EX_STDIN))) {
 			if(passthru_thread_running)
 				passthru_socket_activate(false);
 			else
@@ -848,13 +845,14 @@ int sbbs_t::external(const char* cmdline, long mode, const char* startup_dir)
 
 	if(!(mode&EX_OFFLINE)) {	/* !off-line execution */
 
-		if(native) {
-			if(!(mode&EX_STDIN)) {
-				if(passthru_thread_running)
-					passthru_socket_activate(false);
-				else
-					pthread_mutex_unlock(&input_thread_mutex);
-			}
+		if(!WaitForOutbufEmpty(5000))
+			lprintf(LOG_WARNING, "%s Timeout waiting for output buffer to empty", __FUNCTION__);
+
+		if(native && !(mode & EX_STDIN)) {
+			if(passthru_thread_running)
+				passthru_socket_activate(false);
+			else
+				pthread_mutex_unlock(&input_thread_mutex);
 		}
 
 		curatr=~0;			// Can't guarantee current attributes
@@ -1525,13 +1523,11 @@ int sbbs_t::external(const char* cmdline, long mode, const char* startup_dir)
 #endif
 	}
 
-	if(!(mode&EX_STDIN)) {
-		if(!(mode&EX_STDIN)) {
-			if(passthru_thread_running)
-				passthru_socket_activate(true);
-			else
-				pthread_mutex_lock(&input_thread_mutex);
-		}
+	if(!(mode & (EX_STDIN | EX_OFFLINE))) {
+		if(passthru_thread_running)
+			passthru_socket_activate(true);
+		else
+			pthread_mutex_lock(&input_thread_mutex);
 	}
 
 	if(!(mode&EX_NOLOG) && pipe(err_pipe)!=0) {
@@ -1557,7 +1553,7 @@ int sbbs_t::external(const char* cmdline, long mode, const char* startup_dir)
 		winsize.ws_row=rows;
 		winsize.ws_col=cols;
 		if((pid=forkpty(&in_pipe[1],NULL,&term,&winsize))==-1) {
-			if(!(mode&EX_STDIN)) {
+			if(!(mode & (EX_STDIN | EX_OFFLINE))) {
 				if(passthru_thread_running)
 					passthru_socket_activate(false);
 				else
@@ -1582,7 +1578,7 @@ int sbbs_t::external(const char* cmdline, long mode, const char* startup_dir)
 
 
 		if((pid=FORK())==-1) {
-			if(!(mode&EX_STDIN)) {
+			if(!(mode & (EX_STDIN | EX_OFFLINE))) {
 				if(passthru_thread_running)
 					passthru_socket_activate(false);
 				else
@@ -1868,6 +1864,16 @@ int sbbs_t::external(const char* cmdline, long mode, const char* startup_dir)
 	}
 	if(!(mode&EX_OFFLINE)) {	/* !off-line execution */
 
+		if(!WaitForOutbufEmpty(5000))
+			lprintf(LOG_WARNING, "%s Timeout waiting for output buffer to empty", __FUNCTION__);
+
+		if(!(mode&EX_STDIN)) {
+			if(passthru_thread_running)
+				passthru_socket_activate(false);
+			else
+				pthread_mutex_unlock(&input_thread_mutex);
+		}
+
 		curatr=~0;			// Can't guarantee current attributes
 		attr(LIGHTGRAY);	// Force to "normal"
 
@@ -1880,19 +1886,12 @@ int sbbs_t::external(const char* cmdline, long mode, const char* startup_dir)
 	if(!(mode&EX_NOLOG))
 		close(err_pipe[0]);
 
-	if(!(mode&EX_STDIN)) {
-		if(passthru_thread_running)
-			passthru_socket_activate(false);
-		else
-			pthread_mutex_unlock(&input_thread_mutex);
-	}
-
 	return(errorlevel = WEXITSTATUS(i));
 }
 
 #endif	/* !WIN32 */
 
-const char* quoted_string(const char* str, char* buf, size_t maxlen)
+static const char* quoted_string(const char* str, char* buf, size_t maxlen)
 {
 	if(strchr(str,' ')==NULL)
 		return(str);
@@ -2109,176 +2108,3 @@ char* sbbs_t::cmdstr(const char *instr, const char *fpath, const char *fspec, ch
 
     return(cmd);
 }
-
-/****************************************************************************/
-/* Returns command line generated from instr with %c replacments            */
-/* This is the C-exported version											*/
-/****************************************************************************/
-extern "C"
-char* DLLCALL cmdstr(scfg_t* cfg, user_t* user, const char* instr, const char* fpath
-						,const char* fspec, char* cmd)
-{
-	char	str[MAX_PATH+1];
-    int		i,j,len;
-	static char	buf[512];
-
-	if(cmd==NULL)	cmd=buf;
-    len=strlen(instr);
-	int maxlen = (int)sizeof(buf) - 1;
-    for(i=j=0; i<len && j < maxlen; i++) {
-        if(instr[i]=='%') {
-            i++;
-            cmd[j]=0;
-			int avail = maxlen - j;
-			char ch=instr[i];
-			if(IS_ALPHA(ch))
-				ch=toupper(ch);
-            switch(ch) {
-                case 'A':   /* User alias */
-					if(user!=NULL)
-						strncat(cmd,QUOTED_STRING(instr[i],user->alias,str,sizeof(str)), avail);
-                    break;
-                case 'B':   /* Baud (DTE) Rate */
-                    break;
-                case 'C':   /* Connect Description */
-					if(user!=NULL)
-						strncat(cmd,user->modem, avail);
-                    break;
-                case 'D':   /* Connect (DCE) Rate */
-                    break;
-                case 'E':   /* Estimated Rate */
-                    break;
-                case 'F':   /* File path */
-                    strncat(cmd,QUOTED_STRING(instr[i],fpath,str,sizeof(str)), avail);
-                    break;
-                case 'G':   /* Temp directory */
-                    strncat(cmd,cfg->temp_dir, avail);
-                    break;
-                case 'H':   /* Port Handle or Hardware Flow Control */
-                    break;
-                case 'I':   /* IP address */
-					if(user!=NULL)
-						strncat(cmd,user->note, avail);
-                    break;
-                case 'J':
-                    strncat(cmd,cfg->data_dir, avail);
-                    break;
-                case 'K':
-                    strncat(cmd,cfg->ctrl_dir, avail);
-                    break;
-                case 'L':   /* Lines per message */
-					if(user!=NULL)
-						strncat(cmd,ultoa(cfg->level_linespermsg[user->level],str,10), avail);
-                    break;
-                case 'M':   /* Minutes (credits) for user */
-					if(user!=NULL)
-						strncat(cmd,ultoa(user->min,str,10), avail);
-                    break;
-                case 'N':   /* Node Directory (same as SBBSNODE environment var) */
-                    strncat(cmd,cfg->node_dir, avail);
-                    break;
-                case 'O':   /* SysOp */
-                    strncat(cmd,QUOTED_STRING(instr[i],cfg->sys_op,str,sizeof(str)), avail);
-                    break;
-                case 'P':   /* Client protocol */
-                    break;
-                case 'Q':   /* QWK ID */
-                    strncat(cmd,cfg->sys_id, avail);
-                    break;
-                case 'R':   /* Rows */
-					if(user!=NULL)
-						strncat(cmd,ultoa(user->rows,str,10), avail);
-                    break;
-                case 'S':   /* File Spec */
-                    strncat(cmd, fspec, avail);
-                    break;
-                case 'T':   /* Time left in seconds */
-                    break;
-                case 'U':   /* UART I/O Address (in hex) */
-                    strncat(cmd,ultoa(cfg->com_base,str,16), avail);
-                    break;
-                case 'V':   /* Synchronet Version */
-                    sprintf(str,"%s%c",VERSION,REVISION);
-					strncat(cmd,str, avail);
-                    break;
-                case 'W':   /* Columns/width */
-                    break;
-                case 'X':
-					if(user!=NULL)
-						strncat(cmd,cfg->shell[user->shell]->code, avail);
-                    break;
-                case '&':   /* Address of msr */
-                    break;
-                case 'Y':
-                    break;
-                case 'Z':
-                    strncat(cmd,cfg->text_dir, avail);
-                    break;
-				case '~':	/* DOS-compatible (8.3) filename */
-#ifdef _WIN32
-					char sfpath[MAX_PATH+1];
-					SAFECOPY(sfpath,fpath);
-					GetShortPathName(fpath,sfpath,sizeof(sfpath));
-					strncat(cmd,sfpath, avail);
-#else
-                    strncat(cmd,QUOTED_STRING(instr[i],fpath,str,sizeof(str)), avail);
-#endif
-					break;
-                case '!':   /* EXEC Directory */
-                    strncat(cmd,cfg->exec_dir, avail);
-                    break;
-                case '@':   /* EXEC Directory for DOS/OS2/Win32, blank for Unix */
-#ifndef __unix__
-                    strncat(cmd,cfg->exec_dir, avail);
-#endif
-                    break;
-
-                case '#':   /* Node number (same as SBBSNNUM environment var) */
-                    sprintf(str,"%d",cfg->node_num);
-                    strncat(cmd,str, avail);
-                    break;
-                case '*':
-                    sprintf(str,"%03d",cfg->node_num);
-                    strncat(cmd,str, avail);
-                    break;
-                case '$':   /* Credits */
-					if(user!=NULL)
-						strncat(cmd,ultoa(user->cdt+user->freecdt,str,10), avail);
-                    break;
-                case '%':   /* %% for percent sign */
-                    strncat(cmd,"%", avail);
-                    break;
-				case '.':	/* .exe for DOS/OS2/Win32, blank for Unix */
-#ifndef __unix__
-					strncat(cmd,".exe", avail);
-#endif
-					break;
-				case '?':	/* Platform */
-#ifdef __OS2__
-					strcpy(str,"OS2");
-#else
-					strcpy(str,PLATFORM_DESC);
-#endif
-					strlwr(str);
-					strncat(cmd,str, avail);
-					break;
-				case '^':	/* Architecture */
-					strncat(cmd, ARCHITECTURE_DESC, avail);
-					break;
-                default:    /* unknown specification */
-                    if(IS_DIGIT(instr[i]) && user!=NULL) {
-                        sprintf(str,"%0*d",instr[i]&0xf,user->number);
-                        strncat(cmd,str, avail);
-					}
-                    break;
-			}
-            j=strlen(cmd);
-		}
-        else
-            cmd[j++]=instr[i];
-	}
-    cmd[j]=0;
-
-    return(cmd);
-}
-
diff --git a/src/sbbs3/xtrn_sec.cpp b/src/sbbs3/xtrn_sec.cpp
index 2ee165883fd27925e60b07cbfea35281b2dc51ae..b36266b572920e2c331ff7878e49aaa8999c43f1 100644
--- a/src/sbbs3/xtrn_sec.cpp
+++ b/src/sbbs3/xtrn_sec.cpp
@@ -504,7 +504,7 @@ void sbbs_t::xtrndat(const char *name, const char *dropdir, uchar type, ulong tl
 		if(p)
 			*(p++)=0;
 		else
-			p=nulstr;
+			p=(char*)nulstr;
 
 		safe_snprintf(str, sizeof(str), "%s\n%s\n%s\nCOM%d\n%lu BAUD,N,8,1\n%u\n"
 			,cfg.sys_name						/* Name of BBS */
@@ -522,7 +522,7 @@ void sbbs_t::xtrndat(const char *name, const char *dropdir, uchar type, ulong tl
 		if(p)
 			*(p++)=0;
 		else
-			p=nulstr;
+			p=(char*)nulstr;
 		safe_snprintf(str, sizeof(str), "%s\n%s\n%s\n%d\n%u\n%lu\n"
 			,tmp								/* User's firstname */
 			,p									/* User's lastname */
@@ -1577,8 +1577,6 @@ bool sbbs_t::exec_xtrn(uint xtrnnum)
 
 	SAFEPRINTF(str,"%shangup.now",cfg.node_dir);
 	(void)removecase(str);
-	SAFEPRINTF2(str,"%sfile/%04u.dwn",cfg.data_dir,useron.number);
-	(void)removecase(str);
 
 	mode=0; 	
 	if(cfg.xtrn[xtrnnum]->misc&XTRN_SH)
@@ -1629,9 +1627,6 @@ bool sbbs_t::exec_xtrn(uint xtrnnum)
 			errormsg(WHERE,ERR_OPEN,str,O_WRONLY|O_CREAT|O_APPEND);
 	}
 
-	SAFEPRINTF2(str,"%sfile/%04u.dwn",cfg.data_dir,useron.number);
-	batch_add_list(str);
-
 	SAFEPRINTF(str,"%shangup.now",cfg.node_dir);
 	if(fexistcase(str)) {
 		lprintf(LOG_NOTICE,"Node %d External program requested hangup (%s signaled)"
diff --git a/src/smblib/smbadd.c b/src/smblib/smbadd.c
index 0e7b6df8bfaf881db15165ebbe2c09bd4551b368..c82f9ff9d3bf6c498322c01099f40b90683358bc 100644
--- a/src/smblib/smbadd.c
+++ b/src/smblib/smbadd.c
@@ -1,7 +1,5 @@
 /* Synchronet message base (SMB) high-level "add message" function */
 
-/* $Id: smbadd.c,v 1.46 2020/04/12 06:09:33 rswindell Exp $ */
-
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
  * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
@@ -15,21 +13,9 @@
  * See the GNU Lesser General Public License for more details: lgpl.txt or	*
  * http://www.fsf.org/copyleft/lesser.html									*
  *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
- *																			*
  * For Synchronet coding style and modification guidelines, see				*
  * http://www.synchro.net/source.html										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
@@ -39,22 +25,23 @@
 #include "genwrap.h"
 #include "crc32.h"
 #include "lzh.h"
+#include "datewrap.h"
 
 /****************************************************************************/
 /****************************************************************************/
-int SMBCALL smb_addmsg(smb_t* smb, smbmsg_t* msg, int storage, long dupechk_hashes
+int smb_addmsg(smb_t* smb, smbmsg_t* msg, int storage, long dupechk_hashes
 					   ,uint16_t xlat, const uchar* body, const uchar* tail)
 {
 	uchar*		lzhbuf=NULL;
 	long		lzhlen;
 	int			retval;
 	size_t		n;
-	size_t		l;
+	off_t		l;
 	off_t		length;
 	size_t		taillen=0;
 	size_t		bodylen=0;
 	size_t		chklen=0;
-	long		offset;
+	off_t		offset;
 	uint32_t	crc=0xffffffff;
 	hash_t		found;
 	hash_t**	hashes=NULL;	/* This is a NULL-terminated list of hashes */
@@ -83,7 +70,7 @@ int SMBCALL smb_addmsg(smb_t* smb, smbmsg_t* msg, int storage, long dupechk_hash
 			break;
 
 		msg->hdr.number=smb->status.last_msg+1;
-		if(!(smb->status.attr&(SMB_EMAIL|SMB_NOHASH))) {
+		if(!(smb->status.attr&(SMB_EMAIL | SMB_NOHASH | SMB_FILE_DIRECTORY))) {
 
 			hashes=smb_msghashes(msg,body,SMB_HASH_SOURCE_DUPE);
 
@@ -111,7 +98,7 @@ int SMBCALL smb_addmsg(smb_t* smb, smbmsg_t* msg, int storage, long dupechk_hash
 
 			/* Calculate CRC-32 of message text (before encoding, if any) */
 			if(smb->status.max_crcs && dupechk_hashes&(1<<SMB_HASH_SOURCE_BODY)) {
-				for(l=0;l<chklen;l++)
+				for(l=0;l<(off_t)chklen;l++)
 					crc=ucrc32(body[l],crc); 
 				crc=~crc;
 
@@ -164,12 +151,16 @@ int SMBCALL smb_addmsg(smb_t* smb, smbmsg_t* msg, int storage, long dupechk_hash
 			}
 
 			if(offset<0) {
-				retval=offset;
+				retval=(int)offset;
 				break;
 			}
-			msg->hdr.offset=offset;
+			msg->hdr.offset=(uint32_t)offset;
 
-			smb_fseek(smb->sdt_fp,offset,SEEK_SET);
+			if(smb_fseek(smb->sdt_fp,offset,SEEK_SET) != 0) {
+				sprintf(smb->last_error, "%s seek error %d", __FUNCTION__, errno);
+				retval=SMB_ERR_SEEK;
+				break;
+			}
 
 			if(bodylen) {
 				if((retval=smb_dfield(msg,TEXT_BODY,bodylen))!=SMB_SUCCESS)
@@ -298,10 +289,10 @@ int SMBCALL smb_addmsg(smb_t* smb, smbmsg_t* msg, int storage, long dupechk_hash
 			}
 		}
 
-		if(!(smb->status.attr&(SMB_EMAIL|SMB_NOHASH))
+		if(!(smb->status.attr&(SMB_EMAIL | SMB_NOHASH | SMB_FILE_DIRECTORY))
 			&& smb_addhashes(smb,hashes,/* skip_marked? */FALSE)==SMB_SUCCESS)
 			msg->flags|=MSG_FLAG_HASHED;
-		if(msg->to==NULL)	/* no recipient, don't add header (required for bulkmail) */
+		if(msg->hdr.type == SMB_MSG_TYPE_NORMAL && msg->to == NULL)	/* no recipient, don't add header (required for bulkmail) */
 			break;
 
 		retval=smb_addmsghdr(smb,msg,storage); /* calls smb_unlocksmbhdr() */
@@ -320,7 +311,7 @@ int SMBCALL smb_addmsg(smb_t* smb, smbmsg_t* msg, int storage, long dupechk_hash
 	return(retval);
 }
 
-int SMBCALL smb_addvote(smb_t* smb, smbmsg_t* msg, int storage)
+int smb_addvote(smb_t* smb, smbmsg_t* msg, int storage)
 {
 	int			retval;
 
@@ -358,7 +349,7 @@ int SMBCALL smb_addvote(smb_t* smb, smbmsg_t* msg, int storage)
 	return retval;
 }
 
-int SMBCALL smb_addpoll(smb_t* smb, smbmsg_t* msg, int storage)
+int smb_addpoll(smb_t* smb, smbmsg_t* msg, int storage)
 {
 	int			retval;
 
@@ -398,7 +389,7 @@ int SMBCALL smb_addpoll(smb_t* smb, smbmsg_t* msg, int storage)
 	return retval;
 }
 
-int SMBCALL smb_addpollclosure(smb_t* smb, smbmsg_t* msg, int storage)
+int smb_addpollclosure(smb_t* smb, smbmsg_t* msg, int storage)
 {
 	smbmsg_t	remsg;
 	int			retval;
diff --git a/src/smblib/smballoc.c b/src/smblib/smballoc.c
index f1c00722ecaee9264839f325f0313f9a47312bdb..8396a939d53b74b0566cd36eed3d3a4d6634d4b9 100644
--- a/src/smblib/smballoc.c
+++ b/src/smblib/smballoc.c
@@ -1,7 +1,4 @@
 /* Synchronet message base (SMB) alloc/free routines */
-// vi: tabstop=4
-
-/* $Id: smballoc.c,v 1.14 2019/04/11 01:00:29 rswindell Exp $ */
 
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
@@ -16,21 +13,9 @@
  * See the GNU Lesser General Public License for more details: lgpl.txt or	*
  * http://www.fsf.org/copyleft/lesser.html									*
  *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
- *																			*
  * For Synchronet coding style and modification guidelines, see				*
  * http://www.synchro.net/source.html										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
@@ -46,10 +31,11 @@
 /* smb_close_da() should be called after									*/
 /* Returns negative on error												*/
 /****************************************************************************/
-long SMBCALL smb_allocdat(smb_t* smb, ulong length, uint16_t refs)
+off_t smb_allocdat(smb_t* smb, off_t length, uint16_t refs)
 {
     uint16_t  i;
-	ulong	j,l,blocks,offset=0L;
+	ulong	j,l,blocks;
+	off_t offset=0;
 
 	if(smb->sda_fp==NULL) {
 		safe_snprintf(smb->last_error, sizeof(smb->last_error), "%s msgbase not open", __FUNCTION__);
@@ -75,7 +61,7 @@ long SMBCALL smb_allocdat(smb_t* smb, ulong length, uint16_t refs)
 		return(SMB_ERR_DAT_OFFSET);
 	}
 	clearerr(smb->sda_fp);
-	if(fseek(smb->sda_fp,(offset/SDT_BLOCK_LEN)*sizeof(refs),SEEK_SET)) {
+	if(fseeko(smb->sda_fp,(offset/SDT_BLOCK_LEN)*sizeof(refs),SEEK_SET)) {
 		safe_snprintf(smb->last_error,sizeof(smb->last_error),"%s seeking to: %ld", __FUNCTION__
 			,(offset/SDT_BLOCK_LEN)*sizeof(refs));
 		return(SMB_ERR_SEEK);
@@ -95,9 +81,10 @@ long SMBCALL smb_allocdat(smb_t* smb, ulong length, uint16_t refs)
 /* Allocates space for data, but doesn't search for unused blocks           */
 /* Returns negative on error												*/
 /****************************************************************************/
-long SMBCALL smb_fallocdat(smb_t* smb, ulong length, uint16_t refs)
+off_t smb_fallocdat(smb_t* smb, off_t length, uint16_t refs)
 {
-	ulong	l,blocks,offset;
+	ulong	l,blocks;
+	off_t offset;
 
 	if(smb->sda_fp==NULL) {
 		safe_snprintf(smb->last_error, sizeof(smb->last_error), "%s msgbase not open", __FUNCTION__);
@@ -133,13 +120,13 @@ long SMBCALL smb_fallocdat(smb_t* smb, ulong length, uint16_t refs)
 /* Returns non-zero on error												*/
 /* Always unlocks the SMB header (when not hyper-alloc)						*/
 /****************************************************************************/
-int SMBCALL smb_freemsgdat(smb_t* smb, ulong offset, ulong length, uint16_t refs)
+int smb_freemsgdat(smb_t* smb, off_t offset, ulong length, uint16_t refs)
 {
 	BOOL	da_opened=FALSE;
 	int		retval=SMB_SUCCESS;
 	uint16_t	i;
 	long	l,blocks;
-	ulong	sda_offset;
+	off_t	sda_offset;
 	off_t	flen;
 
 	if(smb->status.attr&SMB_HYPERALLOC)	/* do nothing */
@@ -166,7 +153,7 @@ int SMBCALL smb_freemsgdat(smb_t* smb, ulong offset, ulong length, uint16_t refs
 	// Free from the last block first
 	for(l=blocks-1; l >= 0; l--) {
 		sda_offset=((offset/SDT_BLOCK_LEN)+l)*sizeof(i);
-		if(fseek(smb->sda_fp,sda_offset,SEEK_SET)) {
+		if(fseeko(smb->sda_fp,sda_offset,SEEK_SET)) {
 			safe_snprintf(smb->last_error,sizeof(smb->last_error)
 				,"%s %d '%s' seeking to %lu (0x%lX) of allocation file", __FUNCTION__
 				,get_errno(),STRERROR(get_errno())
@@ -188,7 +175,7 @@ int SMBCALL smb_freemsgdat(smb_t* smb, ulong offset, ulong length, uint16_t refs
 
 		// Completely free? and at end of SDA? Just truncate record from end of file
 		if(i == 0 && ftell(smb->sda_fp) == flen) {
-			if(chsize(fileno(smb->sda_fp), sda_offset) == 0) {
+			if(chsize(fileno(smb->sda_fp), (long)sda_offset) == 0) {
 				flen = sda_offset;
 				continue;
 			}
@@ -211,7 +198,7 @@ int SMBCALL smb_freemsgdat(smb_t* smb, ulong offset, ulong length, uint16_t refs
 	}
 	fflush(smb->sda_fp);
 	if(filelength(fileno(smb->sdt_fp)) / SDT_BLOCK_LEN > (long)(filelength(fileno(smb->sda_fp)) / sizeof(uint16_t)))
-		chsize(fileno(smb->sdt_fp), (filelength(fileno(smb->sda_fp)) / sizeof(uint16_t)) * SDT_BLOCK_LEN);
+		chsize(fileno(smb->sdt_fp), (long)(filelength(fileno(smb->sda_fp)) / sizeof(uint16_t)) * SDT_BLOCK_LEN);
 	if(da_opened)
 		smb_close_da(smb);
 	smb_unlocksmbhdr(smb);
@@ -223,7 +210,7 @@ int SMBCALL smb_freemsgdat(smb_t* smb, ulong offset, ulong length, uint16_t refs
 /* SMB header should be locked before calling this function					*/
 /* Returns non-zero on error												*/
 /****************************************************************************/
-int SMBCALL smb_incdat(smb_t* smb, ulong offset, ulong length, uint16_t refs)
+int smb_incdat(smb_t* smb, off_t offset, ulong length, uint16_t refs)
 {
 	uint16_t	i;
 	ulong	l,blocks;
@@ -235,7 +222,7 @@ int SMBCALL smb_incdat(smb_t* smb, ulong offset, ulong length, uint16_t refs)
 	clearerr(smb->sda_fp);
 	blocks=smb_datblocks(length);
 	for(l=0;l<blocks;l++) {
-		if(fseek(smb->sda_fp,((offset/SDT_BLOCK_LEN)+l)*sizeof(i),SEEK_SET)) {
+		if(fseeko(smb->sda_fp,((offset/SDT_BLOCK_LEN)+l)*sizeof(i),SEEK_SET)) {
 			safe_snprintf(smb->last_error,sizeof(smb->last_error),"%s seeking to %ld", __FUNCTION__
 				,((offset/SDT_BLOCK_LEN)+l)*sizeof(i));
 			return(SMB_ERR_SEEK);
@@ -267,7 +254,7 @@ int SMBCALL smb_incdat(smb_t* smb, ulong offset, ulong length, uint16_t refs)
 /* The opposite function of smb_freemsg()									*/
 /* Always unlocks the SMB header (when not hyper-alloc)						*/
 /****************************************************************************/
-int SMBCALL smb_incmsg_dfields(smb_t* smb, smbmsg_t* msg, uint16_t refs)
+int smb_incmsg_dfields(smb_t* smb, smbmsg_t* msg, uint16_t refs)
 {
 	int		i=SMB_SUCCESS;
 	BOOL	da_opened=FALSE;
@@ -302,7 +289,7 @@ int SMBCALL smb_incmsg_dfields(smb_t* smb, smbmsg_t* msg, uint16_t refs)
 /* De-allocates blocks for header record									*/
 /* Returns non-zero on error												*/
 /****************************************************************************/
-int SMBCALL smb_freemsghdr(smb_t* smb, ulong offset, ulong length)
+int smb_freemsghdr(smb_t* smb, off_t offset, ulong length)
 {
 	uchar	c=0;
 	long	l,blocks;
@@ -319,13 +306,13 @@ int SMBCALL smb_freemsghdr(smb_t* smb, ulong offset, ulong length)
 
 	sha_offset = offset/SHD_BLOCK_LEN;
 	if(filelength(fileno(smb->sha_fp)) <= (sha_offset + blocks)) {
-		if(chsize(fileno(smb->sha_fp), sha_offset) == 0) {
-			chsize(fileno(smb->shd_fp), smb->status.header_offset + offset);
+		if(chsize(fileno(smb->sha_fp), (long)sha_offset) == 0) {
+			chsize(fileno(smb->shd_fp), (long)(smb->status.header_offset + offset));
 			return SMB_SUCCESS;
 		}
 	}
 
-	if(fseek(smb->sha_fp, sha_offset, SEEK_SET)) {
+	if(fseeko(smb->sha_fp, sha_offset, SEEK_SET)) {
 		safe_snprintf(smb->last_error,sizeof(smb->last_error),"%s seeking to %ld", __FUNCTION__, (long)sha_offset);
 		return(SMB_ERR_SEEK);
 	}
@@ -340,7 +327,7 @@ int SMBCALL smb_freemsghdr(smb_t* smb, ulong offset, ulong length)
 
 /****************************************************************************/
 /****************************************************************************/
-int SMBCALL smb_freemsg_dfields(smb_t* smb, smbmsg_t* msg, uint16_t refs)
+int smb_freemsg_dfields(smb_t* smb, smbmsg_t* msg, uint16_t refs)
 {
 	int		i;
 	uint16_t	x;
@@ -356,7 +343,7 @@ int SMBCALL smb_freemsg_dfields(smb_t* smb, smbmsg_t* msg, uint16_t refs)
 /****************************************************************************/
 /* Frees all allocated header and data blocks (1 reference) for 'msg'       */
 /****************************************************************************/
-int SMBCALL smb_freemsg(smb_t* smb, smbmsg_t* msg)
+int smb_freemsg(smb_t* smb, smbmsg_t* msg)
 {
 	int 	i;
 
@@ -382,7 +369,7 @@ int SMBCALL smb_freemsg(smb_t* smb, smbmsg_t* msg)
 /* smb_close_ha() should be called after									*/
 /* Returns negative value on error 											*/
 /****************************************************************************/
-long SMBCALL smb_allochdr(smb_t* smb, ulong length)
+off_t smb_allochdr(smb_t* smb, ulong length)
 {
 	uchar	c;
 	ulong	i,l,blocks,offset=0;
@@ -425,7 +412,7 @@ long SMBCALL smb_allochdr(smb_t* smb, ulong length)
 /* Allocates space for header, but doesn't search for unused blocks          */
 /* Returns negative value on error 											*/
 /****************************************************************************/
-long SMBCALL smb_fallochdr(smb_t* smb, ulong length)
+off_t smb_fallochdr(smb_t* smb, ulong length)
 {
 	uchar	c=1;
 	ulong	l,blocks,offset;
@@ -457,7 +444,7 @@ long SMBCALL smb_fallochdr(smb_t* smb, ulong length)
 /* this function should be most likely not be called from anywhere but	*/
 /* smb_addmsghdr()														*/
 /************************************************************************/
-long SMBCALL smb_hallochdr(smb_t* smb)
+off_t smb_hallochdr(smb_t* smb)
 {
 	ulong offset;
 
@@ -488,9 +475,9 @@ long SMBCALL smb_hallochdr(smb_t* smb)
 /* unlocked until all data fields for this message have been written	*/
 /* to the SDT file														*/
 /************************************************************************/
-long SMBCALL smb_hallocdat(smb_t* smb)
+off_t smb_hallocdat(smb_t* smb)
 {
-	long offset;
+	off_t offset;
 
 	if(smb->sdt_fp==NULL) {
 		safe_snprintf(smb->last_error,sizeof(smb->last_error)
@@ -518,5 +505,5 @@ long SMBCALL smb_hallocdat(smb_t* smb)
 	/* Make sure even block boundry */
 	offset+=PAD_LENGTH_FOR_ALIGNMENT(offset,SDT_BLOCK_LEN);
 
-	return(offset);
+	return (long)offset;
 }
diff --git a/src/smblib/smbdefs.h b/src/smblib/smbdefs.h
index 36e7797d958427ffe467922552b1cdaed7b9ee0e..64ec8d0d4d4da66e8e37f59408721c9b8aa9135a 100644
--- a/src/smblib/smbdefs.h
+++ b/src/smblib/smbdefs.h
@@ -31,8 +31,9 @@
 #include "dirwrap.h"	/* MAX_PATH */
 #include "filewrap.h"	/* SH_DENYRW */
 
-/* SMBLIB Headers */
+/* hash lib Headers */
 #include "md5.h"		/* MD5_DIGEST_SIZE */
+#include "sha1.h"
 
 /**********/
 /* Macros */
@@ -77,9 +78,10 @@
 #define SMB_FASTALLOC		1			/* Fast allocation */
 
 										/* status.attr bit flags: */
-#define SMB_EMAIL			1			/* User numbers stored in Indexes */
-#define SMB_HYPERALLOC		2			/* No allocation (also storage value for smb_addmsghdr) */
-#define SMB_NOHASH			4			/* Do not calculate or store hashes */
+#define SMB_EMAIL			(1<<0)		/* User numbers stored in Indexes */
+#define SMB_HYPERALLOC		(1<<1)		/* No allocation (also storage value for smb_addmsghdr) */
+#define SMB_NOHASH			(1<<2)		/* Do not calculate or store hashes */
+#define SMB_FILE_DIRECTORY	(1<<3)		/* Storage for a file area (for file transfers/downloads) */
 
 										/* Time zone macros for when_t.zone */
 #define DAYLIGHT			0x8000		/* Daylight savings is active */
@@ -197,6 +199,36 @@
 #define	SMB_EDITOR			0x68	/* Associated with FTN ^aNOTE: control line */
 #define SMB_TAGS			0x69	/* List of tags (ala hash-tags) related to this message */
 #define SMB_TAG_DELIMITER	" "
+
+#define SMB_FILEIDX_NAMELEN	64
+#define SMB_FILENAME		SUBJECT
+#define	SMB_FILEDESC		SMB_SUMMARY
+#define SMB_FILEUPLOADER	SENDER
+
+#define FILEATTACH			0x70
+#define DESTFILE			0x71
+#define FILEATTACHLIST		0x72
+#define DESTFILELIST		0x73
+#define FILEREQUEST 		0x74
+#define FILEPASSWORD		0x75
+#define FILEREQUESTLIST 	0x76
+#define FILEPASSWORDLIST	0x77
+
+#define IMAGEATTACH 		0x80
+#define ANIMATTACH			0x81
+#define FONTATTACH			0x82
+#define SOUNDATTACH 		0x83
+#define PRESENTATTACH		0x84
+#define VIDEOATTACH 		0x85
+#define APPDATAATTACH		0x86
+
+#define IMAGETRIGGER		0x90
+#define ANIMTRIGGER 		0x91
+#define FONTTRIGGER 		0x92
+#define SOUNDTRIGGER		0x93
+#define PRESENTTRIGGER		0x94
+#define VIDEOTRIGGER		0x95
+#define APPDATATRIGGER		0x96
 #define SMB_COLUMNS			0x6a	/* original text editor width in fixed-width columns */
 
 #define FIDOCTRL			0xa0
@@ -235,8 +267,8 @@
 
 #define SMB_POLL_ANSWER		0xe0		/* the subject is the question */
 
-#define UNKNOWN 			0xf1
-#define UNKNOWNASCII		0xf2
+#define UNKNOWN 			0xf1		/* specified as 0xf0 in smb.txt/html - oops */
+#define UNKNOWNASCII		0xf2		/* specified as 0xf1 in smb.txt/html - oops */
 #define UNUSED				0xff
 
 										/* Valid dfield_t.types */
@@ -262,6 +294,7 @@
 #define MSG_DOWNVOTE		(1<<12)		/* This message is a downvote */
 #define MSG_POLL			(1<<13)		/* This message is a poll */
 #define MSG_SPAM			(1<<14)		/* This message has been flagged as SPAM */
+#define MSG_FILE			(1<<15)		/* This is a file */
 
 #define MSG_VOTE			(MSG_UPVOTE|MSG_DOWNVOTE)	/* This message is a poll-vote */
 #define MSG_POLL_CLOSURE	(MSG_POLL|MSG_VOTE)			/* This message is a poll-closure */
@@ -269,6 +302,8 @@
 
 #define MSG_POLL_MAX_ANSWERS	16
 
+#define FILE_ANONYMOUS		MSG_ANONYMOUS
+
 										/* Auxiliary header attributes */
 #define MSG_FILEREQUEST 	(1<<0)		/* File request */
 #define MSG_FILEATTACH		(1<<1)		/* File(s) attached to Msg (file paths/names in subject) */
@@ -330,21 +365,9 @@ enum smb_priority {			/* msghdr_t.priority */
 /* Typedefs */
 /************/
 
-#if defined(_WIN32) || defined(__BORLANDC__) 
-	#define PRAGMA_PACK
-#endif
+#pragma pack(push,1)	/* Disk image structures must be packed */
 
-#if defined(PRAGMA_PACK) || defined(__WATCOMC__)
-	#define _PACK
-#else
-	#define _PACK __attribute__ ((packed))
-#endif
-
-#if defined(PRAGMA_PACK)
-	#pragma pack(push,1)	/* Disk image structures must be packed */
-#endif
-
-typedef struct _PACK {		/* Time with time-zone */
+typedef struct {		/* Time with time-zone */
 
 	uint32_t	time;			/* Local time (unix format) */
 	int16_t		zone;			/* Time zone */
@@ -353,17 +376,21 @@ typedef struct _PACK {		/* Time with time-zone */
 
 typedef uint16_t smb_msg_attr_t;
 
-typedef struct _PACK {		/* Index record */
+typedef struct {	/* Index record */
 
 	union {
-		struct _PACK {
-			uint16_t	to; 		/* 16-bit CRC of recipient name (lower case) or user # */
-			uint16_t	from;		/* 16-bit CRC of sender name (lower case) or user # */
-			uint16_t	subj;		/* 16-bit CRC of subject (lower case, w/o RE:) */
+		struct {			/* when msg.type != BALLOT */
+			uint16_t	to; 	/* 16-bit CRC of recipient name (lower case) or user # */
+			uint16_t	from;	/* 16-bit CRC of sender name (lower case) or user # */
+			uint16_t	subj;	/* 16-bit CRC of subject (lower case, w/o RE:) */
+		};
+		struct {			/* when msg.type == BALLOT */
+			uint16_t	votes;	/* votes value */
+			uint32_t	remsg;	/* number of message this vote is in response to */
 		};
-		struct _PACK {
-			uint16_t	votes;		/* votes value */
-			uint32_t	remsg;		/* number of message this vote is in response to */
+		struct {			/* when msg.type == FILE */
+			uint32_t	size;
+			uint16_t	unused;	/* possibly store additional 16-bits of file size here */
 		};
 	};
 	smb_msg_attr_t	attr;		/* attributes (read, permanent, etc.) */
@@ -372,13 +399,40 @@ typedef struct _PACK {		/* Index record */
 	uint32_t	time;			/* time/date message was imported/posted */
 
 } idxrec_t;
+#define SIZEOF_SMB_IDXREC_T 20
 
-										/* valid bits in hash_t.flags		*/
+struct hash_data {
+	uint16_t	crc16;					/* CRC-16 of source */
+	uint32_t	crc32;					/* CRC-32 of source */
+	uint8_t		md5[MD5_DIGEST_SIZE];	/* MD5 digest of source */
+	uint8_t		sha1[SHA1_DIGEST_SIZE];	/* SHA1 hash of source */
+};
+
+typedef uint8_t	hashflags_t;
+
+struct hash_info {
+	hashflags_t flags;
+	struct hash_data data;
+};
+
+typedef struct {		/* File index record */
+	union {
+		idxrec_t	idx;
+		struct {
+			idxrec_t idx_;	// only here for storage, no need to reference
+			char name[SMB_FILEIDX_NAMELEN + 1];
+			struct hash_info hash;
+		};
+	};
+} fileidxrec_t;
+#define SIZEOF_SMB_FILEIDXREC_T 128
+										/* valid bits in hashflags_t		*/
 #define SMB_HASH_CRC16			(1<<0)	/* CRC-16 hash is valid				*/
 #define SMB_HASH_CRC32			(1<<1)	/* CRC-32 hash is valid				*/
 #define SMB_HASH_MD5			(1<<2)	/* MD5 digest is valid				*/
-#define SMB_HASH_MASK			(SMB_HASH_CRC16|SMB_HASH_CRC32|SMB_HASH_MD5)
-								
+#define SMB_HASH_SHA1			(1<<3)	/* SHA1 hsah is valid				*/
+#define SMB_HASH_MASK			(SMB_HASH_CRC16|SMB_HASH_CRC32|SMB_HASH_MD5|SMB_HASH_SHA1)
+
 #define SMB_HASH_MARKED			(1<<4)	/* Used by smb_findhash()			*/
 
 #define SMB_HASH_STRIP_CTRL_A	(1<<5)	/* Strip Ctrl-A codes first			*/
@@ -406,37 +460,43 @@ enum smb_hash_source_type {
 								/* These are the hash sources stored/compared for SPAM message detection: */
 #define SMB_HASH_SOURCE_SPAM	((1<<SMB_HASH_SOURCE_BODY))
 
-typedef struct _PACK {
-
+typedef struct {
 	uint32_t	number;					/* Message number */
 	uint32_t	time;					/* Local time of fingerprinting */
 	uint32_t	length;					/* Length (in bytes) of source */
-	uchar		source;					/* SMB_HASH_SOURCE* (in low 5-bits) */
-	uchar		flags;					/* indications of valid hashes and pre-processing */
-	uint16_t	crc16;					/* CRC-16 of source */
-	uint32_t	crc32;					/* CRC-32 of source */
-	uchar		md5[MD5_DIGEST_SIZE];	/* MD5 digest of source */
-	uchar		reserved[28];			/* sizeof(hash_t) = 64 */
-
+	uint8_t		source;					/* SMB_HASH_SOURCE* (in low 5-bits) */
+	hashflags_t flags;
+	struct hash_data data;
+	uint8_t		reserved[8];			/* sizeof(hash_t) = 64 */
 } hash_t;
+#define SIZEOF_SMB_HASH_T 64
 
-typedef struct _PACK {		/* Message base header (fixed portion) */
+typedef struct {		/* Message base header (fixed portion) */
 
-    uchar		id[LEN_HEADER_ID];	/* SMB<^Z> */
+    uchar		smbhdr_id[LEN_HEADER_ID];	/* SMB<^Z> */
     uint16_t	version;        /* version number (initially 100h for 1.00) */
     uint16_t	length;         /* length including this struct */
 
 } smbhdr_t;
 
-typedef struct _PACK {		/* Message base status header */
+typedef struct {		/* Message/File base status header */
 
-	uint32_t	last_msg;		/* last message number */
-	uint32_t	total_msgs; 	/* total messages */
+	union {
+		uint32_t	last_msg;	/* last message number */
+		uint32_t	last_file;	/* last file number */
+	};
+	union {
+		uint32_t	total_msgs; /* total messages */
+		uint32_t	total_files; /* total files */
+	};
 	uint32_t	header_offset;	/* byte offset to first header record */
 	uint32_t	max_crcs;		/* Maximum number of CRCs to keep in history */
-    uint32_t	max_msgs;       /* Maximum number of message to keep in sub */
-    uint16_t	max_age;        /* Maximum age of message to keep in sub (in days) */
-	uint16_t	attr;			/* Attributes for this message base (SMB_HYPER,etc) */
+	union {
+		uint32_t	max_msgs;	/* Maximum number of message to keep in sub */
+		uint32_t	max_files;	/* Maximum number of files to keep in dir */
+	};
+    uint16_t	max_age;        /* Maximum age of message/file to keep in sub (in days) */
+	uint16_t	attr;			/* Attributes for this message/file base (SMB_HYPER,etc) */
 
 } smbstatus_t;
 
@@ -445,11 +505,12 @@ enum smb_msg_type {
 	,SMB_MSG_TYPE_POLL			/* A poll question  */
 	,SMB_MSG_TYPE_BALLOT		/* Voter response to poll or normal message */
 	,SMB_MSG_TYPE_POLL_CLOSURE	/* Closure of an existing poll */
+	,SMB_MSG_TYPE_FILE			/* A file (e.g. for download) */
 };
 
-typedef struct _PACK {		/* Message header */
+typedef struct {		/* Message/File header */
 
-	/* 00 */ uchar		id[LEN_HEADER_ID];	/* SHD<^Z> */
+	/* 00 */ uchar		msghdr_id[LEN_HEADER_ID];	/* SHD<^Z> */
     /* 04 */ uint16_t	type;				/* Message type (enum smb_msg_type) */
     /* 06 */ uint16_t	version;			/* Version of type (initially 100h for 1.00) */
     /* 08 */ uint16_t	length;				/* Total length of fixed record + all fields */
@@ -462,7 +523,7 @@ typedef struct _PACK {		/* Message header */
     /* 24 */ uint32_t	thread_back;		/* Message number for backwards threading (aka thread_orig) */
     /* 28 */ uint32_t	thread_next;		/* Next message in thread */
     /* 2c */ uint32_t	thread_first;		/* First reply to this message */
-	/* 30 */ uint16_t	delivery_attempts;	/* Delivery attempt counter */
+	/* 30 */ uint16_t	delivery_attempts;	/* Delivery attempt counter (for SMTP) */
 	/* 32 */ int16_t	votes;				/* Votes value (response to poll) or maximum votes per ballot (poll) */
 	/* 34 */ uint32_t	thread_id;			/* Number of original message in thread (or 0 if unknown) */
 	union {	/* 38-3f */
@@ -481,7 +542,7 @@ typedef struct _PACK {		/* Message header */
 
 #define thread_orig	thread_back	/* for backwards compatibility with older code */
 
-typedef struct _PACK {		/* Data field */
+typedef struct {		/* Data field */
 
 	uint16_t	type;			/* Type of data field */
     uint32_t	offset;         /* Offset into buffer */ 
@@ -489,14 +550,14 @@ typedef struct _PACK {		/* Data field */
 
 } dfield_t;
 
-typedef struct _PACK {		/* Header field */
+typedef struct {		/* Header field */
 
 	uint16_t	type;
 	uint16_t	length; 		/* Length of buffer */
 
 } hfield_t;
 
-typedef struct _PACK {		/* FidoNet address (zone:net/node.point) */
+typedef struct {		/* FidoNet address (zone:net/node.point) */
 
 	uint16_t	zone;
 	uint16_t	net;
@@ -505,9 +566,7 @@ typedef struct _PACK {		/* FidoNet address (zone:net/node.point) */
 
 } fidoaddr_t;
 
-#if defined(PRAGMA_PACK)
 #pragma pack(pop)		/* original packing */
-#endif
 
 typedef uint16_t smb_net_type_t;
 
@@ -521,9 +580,12 @@ typedef struct {		/* Network (type and address) */
 								/* Valid bits in smbmsg_t.flags					*/
 #define MSG_FLAG_HASHED	(1<<0)	/* Message has been hashed with smb_hashmsg()	*/
 
-typedef struct {				/* Message */
+typedef struct {				/* Message or File */
 
-	idxrec_t	idx;			/* Index */
+	union {						/* Index */
+		idxrec_t		idx;
+		fileidxrec_t	file_idx;
+	};
 	msghdr_t	hdr;			/* Header record (fixed portion) */
 	char		*to,			/* To name */
 				*to_ext,		/* To extension */
@@ -552,8 +614,18 @@ typedef struct {				/* Message */
 				*ftn_bbsid,		/* FTN BBSID */
 				*ftn_msgid,		/* FTN MSGID */
 				*ftn_reply;		/* FTN REPLY */
-	char*		summary;		/* Summary  */
-	char*		subj;			/* Subject  */
+	union {
+		char*	summary;		/* Message Summary  */
+		char*	desc;			/* File description */
+	};
+	union {
+		char*	subj;			/* Subject  */
+		char*	name;			/* Filename */
+	};
+	union {
+		uchar*	text;			/* Message body text (optional) */
+		char*	extdesc;		/* File extended description */
+	};
 	char*		tags;			/* Message tags (space-delimited) */
 	char*		editor;			/* Message editor (if known) */
 	char*		mime_version;	/* MIME Version (if applicable) */
@@ -571,7 +643,7 @@ typedef struct {				/* Message */
 	hfield_t	*hfield;		/* Header fields (fixed length portion) */
 	void		**hfield_dat;	/* Header fields (variable length portion) */
 	dfield_t	*dfield;		/* Data fields (fixed length portion) */
-	int32_t		offset; 		/* Offset (number of records) into index */
+	int32_t		idx_offset;		/* Offset (number of records) into index */
 	BOOL		forwarded;		/* Forwarded from agent to another */
 	uint32_t	expiration; 	/* Message will expire on this day (if >0) */
 	uint32_t	cost;			/* Cost to download/read */
@@ -580,11 +652,15 @@ typedef struct {				/* Message */
 	uint32_t	upvotes;		/* Vote tally for this message */
 	uint32_t	downvotes;		/* Vote tally for this message */
 	uint32_t	total_votes;	/* Total votes for this message or poll */
+								/* Not written to the database: */
+	int32_t		dir;			/* Directory number */
+	off_t		size;			/* File size (current) */
+	time_t		time;			/* File modification date/timestamp (current) */
 	uint8_t		columns;		/* 0 means unknown or N/A */
 
-} smbmsg_t;
+} smbmsg_t, smbfile_t;
 
-typedef struct {				/* Message base */
+typedef struct {				/* Message/File base */
 
     char		file[128];      /* Path and base filename (no extension) */
     FILE*		sdt_fp;			/* File pointer for data (.sdt) file */
@@ -597,12 +673,18 @@ typedef struct {				/* Message base */
 	uint32_t	retry_delay;	/* Time-slice yield (milliseconds) while retrying */
 	smbstatus_t status; 		/* Status header record */
 	BOOL		locked;			/* SMB header is locked */
-	BOOL		continue_on_error;			/* Attempt recovery after some normaly fatal errors */
+	BOOL		continue_on_error;			/* Attempt recovery after some normally fatal errors */
 	char		last_error[MAX_PATH*2];		/* Last error message */
 
 	/* Private member variables (not initialized by or used by smblib) */
-	uint32_t	subnum;			/* Sub-board number */
-	uint32_t	msgs;			/* Number of messages loaded (for user) */
+	union {
+		uint32_t	subnum;		/* Sub-board number */
+		uint32_t	dirnum;		/* Directory number */
+	};
+	union {
+		uint32_t	msgs;		/* Number of messages loaded (for user) */
+		uint32_t	files;		/* Number of files loaded */
+	};
 	uint32_t	curmsg;			/* Current message number (for user, 0-based) */
 
 } smb_t;
diff --git a/src/smblib/smbdump.c b/src/smblib/smbdump.c
index 107bd66615deb190cb8f75dea4e9a064d17239b0..ccd753905ab7b26b4c94a08332f9fcac290c0c72 100644
--- a/src/smblib/smbdump.c
+++ b/src/smblib/smbdump.c
@@ -46,7 +46,7 @@ static char *binstr(uchar *buf, uint16_t length)
 	return(str);
 }
 
-str_list_t SMBCALL smb_msghdr_str_list(smbmsg_t* msg)
+str_list_t smb_msghdr_str_list(smbmsg_t* msg)
 {
 	char tmp[128];
 	int i;
@@ -61,6 +61,11 @@ str_list_t SMBCALL smb_msghdr_str_list(smbmsg_t* msg)
 	/* variable fields */
 	for(i=0;i<msg->total_hfields;i++) {
 		switch(msg->hfield[i].type) {
+			case SMB_COST:
+				strListAppendFormat(&list, HFIELD_NAME_FMT "%lu"
+					,smb_hfieldtype(msg->hfield[i].type)
+					,(ulong)*(uint32_t*)msg->hfield_dat[i]);
+				break;
 			case SMB_COLUMNS:
 				strListAppendFormat(&list, HFIELD_NAME_FMT "%u"
 					,smb_hfieldtype(msg->hfield[i].type)
@@ -168,7 +173,7 @@ str_list_t SMBCALL smb_msghdr_str_list(smbmsg_t* msg)
 	return list;
 }
 
-void SMBCALL smb_dump_msghdr(FILE* fp, smbmsg_t* msg)
+void smb_dump_msghdr(FILE* fp, smbmsg_t* msg)
 {
 	int i;
 
diff --git a/src/smblib/smbfile.c b/src/smblib/smbfile.c
index c1269cb726f87e177e0f940595a104415957cf63..f96426376b6296018020188651aa42589ebeb5fc 100644
--- a/src/smblib/smbfile.c
+++ b/src/smblib/smbfile.c
@@ -1,6 +1,4 @@
-/* Synchronet message base (SMB) FILE stream I/O routines */
-
-/* $Id: smbfile.c,v 1.17 2020/04/14 07:08:50 rswindell Exp $ */
+/* Synchronet message base (SMB) FILE stream and FileBase routines  */
 
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
@@ -15,82 +13,70 @@
  * See the GNU Lesser General Public License for more details: lgpl.txt or	*
  * http://www.fsf.org/copyleft/lesser.html									*
  *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
- *																			*
  * For Synchronet coding style and modification guidelines, see				*
  * http://www.synchro.net/source.html										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
 #include "smblib.h"
 
-int SMBCALL smb_feof(FILE* fp)
+int smb_feof(FILE* fp)
 {
 	return(feof(fp));
 }
 
-int SMBCALL smb_ferror(FILE* fp)
+int smb_ferror(FILE* fp)
 {
 	return(ferror(fp));
 }
 
-int SMBCALL smb_fflush(FILE* fp)
+int smb_fflush(FILE* fp)
 {
 	return(fflush(fp));
 }
 
-int SMBCALL smb_fgetc(FILE* fp)
+int smb_fgetc(FILE* fp)
 {
 	return(fgetc(fp));
 }
 
-int SMBCALL smb_fputc(int ch, FILE* fp)
+int smb_fputc(int ch, FILE* fp)
 {
 	return(fputc(ch,fp));
 }
 
-int SMBCALL smb_fseek(FILE* fp, long offset, int whence)
+int smb_fseek(FILE* fp, off_t offset, int whence)
 {
-	return(fseek(fp,offset,whence));
+	return(fseeko(fp,offset,whence));
 }
 
-long SMBCALL smb_ftell(FILE* fp)
+off_t smb_ftell(FILE* fp)
 {
-	return(ftell(fp));
+	return(ftello(fp));
 }
 
-long SMBCALL smb_fgetlength(FILE* fp)
+off_t smb_fgetlength(FILE* fp)
 {
 	return(filelength(fileno(fp)));
 }
 
-int SMBCALL smb_fsetlength(FILE* fp, long length)
+int smb_fsetlength(FILE* fp, long length)
 {
 	return(chsize(fileno(fp),length));
 }
 
-void SMBCALL smb_rewind(FILE* fp)
+void smb_rewind(FILE* fp)
 {
 	rewind(fp);
 }
 
-void SMBCALL smb_clearerr(FILE* fp)
+void smb_clearerr(FILE* fp)
 {
 	clearerr(fp);
 }
 
-size_t SMBCALL smb_fread(smb_t* smb, void* buf, size_t bytes, FILE* fp)
+size_t smb_fread(smb_t* smb, void* buf, size_t bytes, FILE* fp)
 {
 	size_t ret;
 	time_t start=0;
@@ -114,7 +100,7 @@ size_t SMBCALL smb_fread(smb_t* smb, void* buf, size_t bytes, FILE* fp)
 	#pragma argsused
 #endif
 
-size_t SMBCALL smb_fwrite(smb_t* smb, const void* buf, size_t bytes, FILE* fp)
+size_t smb_fwrite(smb_t* smb, const void* buf, size_t bytes, FILE* fp)
 {
 	return(fwrite(buf,1,bytes,fp));
 }
@@ -124,7 +110,7 @@ size_t SMBCALL smb_fwrite(smb_t* smb, const void* buf, size_t bytes, FILE* fp)
 /* Retries for retry_time number of seconds									*/
 /* Return 0 on success, non-zero otherwise									*/
 /****************************************************************************/
-int SMBCALL smb_open_fp(smb_t* smb, FILE** fp, int share)
+int smb_open_fp(smb_t* smb, FILE** fp, int share)
 {
 	int 	file;
 	char	path[MAX_PATH+1];
@@ -188,7 +174,7 @@ int SMBCALL smb_open_fp(smb_t* smb, FILE** fp, int share)
 
 /****************************************************************************/
 /****************************************************************************/
-void SMBCALL smb_close_fp(FILE** fp)
+void smb_close_fp(FILE** fp)
 {
 	if(fp!=NULL) {
 		if(*fp!=NULL)
@@ -196,3 +182,256 @@ void SMBCALL smb_close_fp(FILE** fp)
 		*fp=NULL;
 	}
 }
+
+/****************************************************************************/
+/* maxlen includes the NUL terminator										*/
+/****************************************************************************/
+char* smb_fileidxname(const char* filename, char* buf, size_t maxlen)
+{
+	size_t fnlen = strlen(filename);
+	char* ext = getfext(filename);
+	if(ext != NULL) {
+		size_t extlen = strlen(ext);
+		if(extlen >= maxlen - 1) {
+			strncpy(buf, filename, maxlen);
+			buf[maxlen - 1] = '\0';
+		}
+		else {
+			fnlen -= extlen;
+			if(fnlen > (maxlen - 1) - extlen)
+				fnlen = (maxlen - 1) - extlen;
+			safe_snprintf(buf, maxlen, "%-.*s%s", (int)fnlen, filename, ext);
+		}
+	} else {	/* no extension */
+		strncpy(buf, filename, maxlen);
+		buf[maxlen - 1] = '\0';
+	}
+	return buf;
+}
+
+/****************************************************************************/
+/* Find file in index via either/or:										*/
+/* -  CASE-INSENSITIVE 'filename' search through index (no wildcards)		*/
+/* -  file content size and hash details found in 'file'					*/
+/****************************************************************************/
+int smb_findfile(smb_t* smb, const char* filename, smbfile_t* file)
+{
+	long offset = 0;
+	smbfile_t* f = file;
+	smbfile_t file_ = {0};
+	if(f == NULL)
+		f = &file_;
+	char fname[SMB_FILEIDX_NAMELEN + 1] = "";
+	if(filename != NULL)
+		smb_fileidxname(filename, fname, sizeof(fname));
+
+	if(smb->sid_fp == NULL) {
+		safe_snprintf(smb->last_error, sizeof(smb->last_error), "%s msgbase not open", __FUNCTION__);
+		return SMB_ERR_NOT_OPEN;
+	}
+	f->dir = smb->dirnum;
+	rewind(smb->sid_fp);
+	while(!feof(smb->sid_fp)) {
+		fileidxrec_t fidx;
+
+		if(smb_fread(smb, &fidx, sizeof(fidx), smb->sid_fp) != sizeof(fidx))
+			break;
+
+		f->idx_offset = offset++;
+
+		if(filename != NULL) {
+			if(stricmp(fidx.name, fname) != 0)
+				continue;
+			f->file_idx = fidx;
+			return SMB_SUCCESS;
+		}
+
+		if(file == NULL)
+			continue;
+
+		if((f->file_idx.hash.flags & SMB_HASH_MASK) != 0 || f->file_idx.idx.size > 0) {
+			if(f->file_idx.idx.size > 0 && f->file_idx.idx.size != fidx.idx.size)
+				continue;
+			if((f->file_idx.hash.flags & SMB_HASH_CRC16) && f->file_idx.hash.data.crc16 != fidx.hash.data.crc16)
+				continue;
+			if((f->file_idx.hash.flags & SMB_HASH_CRC32) && f->file_idx.hash.data.crc32 != fidx.hash.data.crc32)
+				continue;
+			if((f->file_idx.hash.flags & SMB_HASH_MD5)
+				&& memcmp(f->file_idx.hash.data.md5, fidx.hash.data.md5, sizeof(fidx.hash.data.md5)) !=0)
+				continue;
+			if((f->file_idx.hash.flags & SMB_HASH_SHA1)
+				&& memcmp(f->file_idx.hash.data.sha1, fidx.hash.data.sha1, sizeof(fidx.hash.data.sha1)) !=0)
+				continue;
+			f->file_idx = fidx;
+			return SMB_SUCCESS;
+		}
+	}
+	return SMB_ERR_NOT_FOUND;
+}
+
+/****************************************************************************/
+/****************************************************************************/
+int smb_loadfile(smb_t* smb, const char* filename, smbfile_t* file, enum file_detail detail)
+{
+	int result;
+
+	memset(file, 0, sizeof(*file));
+
+	if((result = smb_findfile(smb, filename, file)) != SMB_SUCCESS)
+		return result;
+
+	return smb_getfile(smb, file, detail);
+}
+
+/****************************************************************************/
+/****************************************************************************/
+int smb_getfile(smb_t* smb, smbfile_t* file, enum file_detail detail)
+{
+	int result;
+
+	file->name = file->file_idx.name;
+	file->hdr.when_written.time = file->idx.time;
+	if(detail > file_detail_index) {
+		if((result = smb_getmsghdr(smb, file)) != SMB_SUCCESS)
+			return result;
+		if(detail >= file_detail_extdesc)
+			file->extdesc = smb_getmsgtxt(smb, file, GETMSGTXT_ALL);
+	}
+	file->dir = smb->dirnum;
+
+	return SMB_SUCCESS;
+}
+
+/****************************************************************************/
+/* Writes both header and index information for file 'file'                 */
+/* Like smb_putmsg() but doesn't (re)-initialize index (idx)				*/
+/****************************************************************************/
+int smb_putfile(smb_t* smb, smbfile_t* file)
+{
+	int result;
+
+	if((result = smb_putmsghdr(smb, file))!=SMB_SUCCESS)
+		return result;
+
+	return smb_putmsgidx(smb, file);
+}
+
+/****************************************************************************/
+/****************************************************************************/
+void smb_freefilemem(smbfile_t* file)
+{
+	smb_freemsgmem(file);
+}
+
+/****************************************************************************/
+/****************************************************************************/
+int smb_addfile(smb_t* smb, smbfile_t* file, int storage, const char* extdesc, const char* path)
+{
+	if(file->name == NULL || *file->name == '\0') {
+		safe_snprintf(smb->last_error, sizeof(smb->last_error), "%s missing name", __FUNCTION__);
+		return SMB_ERR_HDR_FIELD;
+	}
+	if(smb_findfile(smb, file->name, NULL) == SMB_SUCCESS) {
+		safe_snprintf(smb->last_error, sizeof(smb->last_error), "%s duplicate name found: %s", __FUNCTION__, file->name);
+		return SMB_DUPE_MSG;
+	}
+	if(path != NULL) {
+		file->size = flength(path);
+		file->hdr.when_written.time = (uint32_t)fdate(path);
+		if(!(smb->status.attr & SMB_NOHASH) && file->file_idx.hash.flags == 0)
+			file->file_idx.hash.flags = smb_hashfile(path, file->size, &file->file_idx.hash.data);
+	}
+	file->hdr.attr |= MSG_FILE;
+	file->hdr.type = SMB_MSG_TYPE_FILE;
+	return smb_addmsg(smb, file, storage, SMB_HASH_SOURCE_NONE, XLAT_NONE, /* body: */(const uchar*)extdesc, /* tail: */NULL);
+}
+
+/****************************************************************************/
+/****************************************************************************/
+int smb_renewfile(smb_t* smb, smbfile_t* file, int storage, const char* path)
+{
+	int result;
+	if((result = smb_removefile(smb, file)) != SMB_SUCCESS)
+		return result;
+	return smb_addfile(smb, file, storage, file->extdesc, path);
+}
+
+/****************************************************************************/
+/****************************************************************************/
+int smb_removefile(smb_t* smb, smbfile_t* file)
+{
+	int result;
+	int removed = 0;
+
+	if(!smb->locked && smb_locksmbhdr(smb) != SMB_SUCCESS)
+		return SMB_ERR_LOCK;
+
+	file->hdr.attr |= MSG_DELETE;
+	if((result = smb_putmsghdr(smb, file)) != SMB_SUCCESS) {
+		smb_unlocksmbhdr(smb);
+		return result;
+	}
+	if((result = smb_getstatus(smb)) != SMB_SUCCESS) {
+		smb_unlocksmbhdr(smb);
+		return result;
+	}
+	if((result = smb_open_ha(smb)) != SMB_SUCCESS) {
+		smb_unlocksmbhdr(smb);
+		return result;
+	}
+	if((result = smb_open_da(smb)) != SMB_SUCCESS) {
+		smb_unlocksmbhdr(smb);
+		return result;
+	}
+	result = smb_freemsg(smb, file);
+	smb_close_ha(smb);
+	smb_close_da(smb);
+
+	// Now remove from index:
+	if(result == SMB_SUCCESS) {
+		rewind(smb->sid_fp);
+		fileidxrec_t* fidx = malloc(smb->status.total_files * sizeof(*fidx));
+		if(fidx == NULL) {
+			smb_unlocksmbhdr(smb);
+			return SMB_ERR_MEM;
+		}
+		if(fread(fidx, sizeof(*fidx), smb->status.total_files, smb->sid_fp) != smb->status.total_files) {
+			free(fidx);
+			smb_unlocksmbhdr(smb);
+			return SMB_ERR_READ;
+		}
+		rewind(smb->sid_fp);
+		for(uint32_t i = 0; i < smb->status.total_files; i++) {
+			if(stricmp(fidx[i].name, file->name) == 0) {
+				removed++;
+				continue;
+			}
+			if(fwrite(fidx + i, sizeof(*fidx), 1, smb->sid_fp) != 1) {
+				safe_snprintf(smb->last_error, sizeof(smb->last_error), "%s re-writing index"
+					,__FUNCTION__);
+				result = SMB_ERR_WRITE;
+				break;
+			}
+		}
+		free(fidx);
+		if(result == SMB_SUCCESS) {
+			if(removed < 1) {
+				safe_snprintf(smb->last_error, sizeof(smb->last_error), "%s name found: %s"
+					,__FUNCTION__, file->name);
+				result = SMB_ERR_NOT_FOUND;
+			} else {
+				fflush(smb->sid_fp);
+				smb->status.total_files -= removed;
+				if(chsize(fileno(smb->sid_fp), smb->status.total_files * sizeof(*fidx)) != 0) {
+					safe_snprintf(smb->last_error, sizeof(smb->last_error), "%s error %d truncating index"
+						,__FUNCTION__, errno);
+					result = SMB_ERR_DELETE;
+				} else
+					result = smb_putstatus(smb);
+			}
+		}
+	}
+
+	smb_unlocksmbhdr(smb);
+	return result;
+}
diff --git a/src/smblib/smbhash.c b/src/smblib/smbhash.c
index 49d3c054b9fbf004719181d2d229c5fe1620265b..e884c5235bccff1536225d4ca73830f8836e7504 100644
--- a/src/smblib/smbhash.c
+++ b/src/smblib/smbhash.c
@@ -1,8 +1,5 @@
 /* Synchronet message base (SMB) hash-related functions */
 
-/* $Id: smbhash.c,v 1.36 2019/04/11 01:00:30 rswindell Exp $ */
-// vi: tabstop=4
-
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
  * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
@@ -16,21 +13,9 @@
  * See the GNU Lesser General Public License for more details: lgpl.txt or	*
  * http://www.fsf.org/copyleft/lesser.html									*
  *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
- *																			*
  * For Synchronet coding style and modification guidelines, see				*
  * http://www.synchro.net/source.html										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
@@ -39,12 +24,13 @@
 #include <ctype.h>		/* isspace()*/
 #include "smblib.h"
 #include "md5.h"
+#include "sha1.h"
 #include "crc16.h"
 #include "crc32.h"
 #include "genwrap.h"
 
 /* If return value is SMB_ERR_NOT_FOUND, hash file is left open */
-int SMBCALL smb_findhash(smb_t* smb, hash_t** compare, hash_t* found_hash, 
+int smb_findhash(smb_t* smb, hash_t** compare, hash_t* found_hash, 
 						 long source_mask, BOOL mark)
 {
 	int		retval;
@@ -87,15 +73,18 @@ int SMBCALL smb_findhash(smb_t* smb, hash_t** compare, hash_t* found_hash,
 				if((compare[c]->flags&hash.flags&SMB_HASH_MASK)==0)	
 					continue;	/* no matching hashes */
 				if((compare[c]->flags&hash.flags&SMB_HASH_CRC16)
-					&& compare[c]->crc16!=hash.crc16)
+					&& compare[c]->data.crc16!=hash.data.crc16)
 					continue;	/* wrong crc-16 */
 				if((compare[c]->flags&hash.flags&SMB_HASH_CRC32)
-					&& compare[c]->crc32!=hash.crc32)
+					&& compare[c]->data.crc32!=hash.data.crc32)
 					continue;	/* wrong crc-32 */
 				if((compare[c]->flags&hash.flags&SMB_HASH_MD5)
-					&& memcmp(compare[c]->md5,hash.md5,sizeof(hash.md5)))
+					&& memcmp(compare[c]->data.md5,hash.data.md5,sizeof(hash.data.md5)))
 					continue;	/* wrong MD5 */
-				
+				if((compare[c]->flags&hash.flags&SMB_HASH_SHA1)
+					&& memcmp(compare[c]->data.sha1,hash.data.sha1,sizeof(hash.data.sha1)))
+					continue;	/* wrong SHA1 */
+
 				/* successful match! */
 				break;	/* can't match more than one, so stop comparing */
 			}
@@ -123,7 +112,7 @@ int SMBCALL smb_findhash(smb_t* smb, hash_t** compare, hash_t* found_hash,
 	return(SMB_ERR_NOT_FOUND);
 }
 
-int SMBCALL smb_addhashes(smb_t* smb, hash_t** hashes, BOOL skip_marked)
+int smb_addhashes(smb_t* smb, hash_t** hashes, BOOL skip_marked)
 {
 	int		retval;
 	size_t	h;
@@ -187,7 +176,7 @@ static char* strip_ctrla(uchar* dst, const uchar* src)
 
 /* Allocates and calculates hashes of data (based on flags)					*/
 /* Returns NULL on failure													*/
-hash_t* SMBCALL smb_hash(ulong msgnum, uint32_t t, unsigned source, unsigned flags
+hash_t* smb_hash(ulong msgnum, uint32_t t, unsigned source, unsigned flags
 						 ,const void* data, size_t length)
 {
 	hash_t*	hash;
@@ -205,11 +194,13 @@ hash_t* SMBCALL smb_hash(ulong msgnum, uint32_t t, unsigned source, unsigned fla
 	hash->source=source;
 	hash->flags=flags;
 	if(flags&SMB_HASH_CRC16)
-		hash->crc16=crc16((char*)data,length);
+		hash->data.crc16=crc16((char*)data,length);
 	if(flags&SMB_HASH_CRC32)
-		hash->crc32=crc32((char*)data,length);
+		hash->data.crc32=crc32((char*)data,length);
 	if(flags&SMB_HASH_MD5)
-		MD5_calc(hash->md5,data,length);
+		MD5_calc(hash->data.md5,data,length);
+	if(flags&SMB_HASH_SHA1)
+		SHA1_calc(hash->data.sha1, data, length);
 
 	return(hash);
 }
@@ -217,7 +208,7 @@ hash_t* SMBCALL smb_hash(ulong msgnum, uint32_t t, unsigned source, unsigned fla
 /* Allocates and calculates hashes of data (based on flags)					*/
 /* Supports string hash "pre-processing" (e.g. lowercase, strip whitespace)	*/
 /* Returns NULL on failure													*/
-hash_t* SMBCALL smb_hashstr(ulong msgnum, uint32_t t, unsigned source, unsigned flags
+hash_t* smb_hashstr(ulong msgnum, uint32_t t, unsigned source, unsigned flags
 							,const char* str)
 {
 	char*	p=NULL;
@@ -245,10 +236,10 @@ hash_t* SMBCALL smb_hashstr(ulong msgnum, uint32_t t, unsigned source, unsigned
 
 /* Allocates and calculates all hashes for a single message					*/
 /* Returns NULL on failure													*/
-hash_t** SMBCALL smb_msghashes(smbmsg_t* msg, const uchar* body, long source_mask)
+hash_t** smb_msghashes(smbmsg_t* msg, const uchar* body, long source_mask)
 {
 	size_t		h=0;
-	uchar		flags=SMB_HASH_CRC16|SMB_HASH_CRC32|SMB_HASH_MD5;
+	uchar		flags=SMB_HASH_CRC16|SMB_HASH_CRC32|SMB_HASH_MD5|SMB_HASH_SHA1;
 	hash_t**	hashes;	/* This is a NULL-terminated list of hashes */
 	hash_t*		hash;
 	time_t		t=time(NULL);
@@ -289,7 +280,7 @@ hash_t** SMBCALL smb_msghashes(smbmsg_t* msg, const uchar* body, long source_mas
 	return(hashes);
 }
 
-void SMBCALL smb_freehashes(hash_t** hashes)
+void smb_freehashes(hash_t** hashes)
 {
 	size_t		n;
 
@@ -297,14 +288,14 @@ void SMBCALL smb_freehashes(hash_t** hashes)
 }
 
 /* Calculates and stores the hashes for a single message					*/
-int SMBCALL smb_hashmsg(smb_t* smb, smbmsg_t* msg, const uchar* text, BOOL update)
+int smb_hashmsg(smb_t* smb, smbmsg_t* msg, const uchar* text, BOOL update)
 {
 	size_t		n;
 	int			retval=SMB_SUCCESS;
 	hash_t		found;
 	hash_t**	hashes;	/* This is a NULL-terminated list of hashes */
 
-	if(smb->status.attr&(SMB_EMAIL|SMB_NOHASH))
+	if(smb->status.attr&(SMB_EMAIL | SMB_NOHASH | SMB_FILE_DIRECTORY))
 		return(SMB_SUCCESS);
 
 	hashes=smb_msghashes(msg,text,SMB_HASH_SOURCE_DUPE);
@@ -326,7 +317,7 @@ int SMBCALL smb_hashmsg(smb_t* smb, smbmsg_t* msg, const uchar* text, BOOL updat
 }
 
 /* length=0 specifies ASCIIZ data											*/
-int SMBCALL smb_getmsgidx_by_hash(smb_t* smb, smbmsg_t* msg, unsigned source
+int smb_getmsgidx_by_hash(smb_t* smb, smbmsg_t* msg, unsigned source
 								 ,unsigned flags, const void* data, size_t length)
 {
 	int			retval;
@@ -360,7 +351,7 @@ int SMBCALL smb_getmsgidx_by_hash(smb_t* smb, smbmsg_t* msg, unsigned source
 	return(retval);
 }
 
-int SMBCALL smb_getmsghdr_by_hash(smb_t* smb, smbmsg_t* msg, unsigned source
+int smb_getmsghdr_by_hash(smb_t* smb, smbmsg_t* msg, unsigned source
 								 ,unsigned flags, const void* data, size_t length)
 {
 	int retval;
@@ -378,7 +369,7 @@ int SMBCALL smb_getmsghdr_by_hash(smb_t* smb, smbmsg_t* msg, unsigned source
 	return(retval);
 }
 
-uint16_t SMBCALL smb_subject_crc(const char* subj)
+uint16_t smb_subject_crc(const char* subj)
 {
 	char*	str;
 	uint16_t	crc;
@@ -402,7 +393,7 @@ uint16_t SMBCALL smb_subject_crc(const char* subj)
 	return(crc);
 }
 
-uint16_t SMBCALL smb_name_crc(const char* name)
+uint16_t smb_name_crc(const char* name)
 {
 	char*	str;
 	uint16_t	crc;
@@ -419,3 +410,38 @@ uint16_t SMBCALL smb_name_crc(const char* name)
 
 	return(crc);
 }
+
+// Returns hashflags_t on success
+int smb_hashfile(const char* path, off_t size, struct hash_data* data)
+{
+	char buf[256 * 1024];
+	FILE*	fp;
+	MD5 md5_ctx;
+	SHA1_CTX sha1_ctx;
+
+	if(size < 1)
+		return 0;
+
+	if((fp = fopen(path, "rb")) == NULL)
+		return 0;
+
+	MD5_open(&md5_ctx);
+	SHA1Init(&sha1_ctx);
+	data->crc16 = 0;
+	data->crc32 = 0;
+	off_t off = 0;
+	while(!feof(fp) && off < size) {
+		size_t rd = fread(buf, sizeof(uint8_t), sizeof(buf), fp);
+		if(rd < 1)
+			break;
+		data->crc32 = crc32i(~data->crc32, buf, rd);
+		data->crc16 = icrc16(data->crc16, buf, rd);
+		MD5_digest(&md5_ctx, buf, rd);
+		SHA1Update(&sha1_ctx, (unsigned char*)buf, rd);
+		off += rd;
+	}
+	fclose(fp);
+	MD5_close(&md5_ctx, data->md5);
+	SHA1Final(&sha1_ctx, data->sha1);
+	return SMB_HASH_CRC16 | SMB_HASH_CRC32 | SMB_HASH_MD5 | SMB_HASH_SHA1;
+}
diff --git a/src/smblib/smblib.c b/src/smblib/smblib.c
index 41b8ae62e25833dc89b0896682d35780c9092dba..36a5659e41a8307675637c390b48d2872d12a47d 100644
--- a/src/smblib/smblib.c
+++ b/src/smblib/smblib.c
@@ -36,18 +36,18 @@
 #include "filewrap.h"
 
 /* Use smb_ver() and smb_lib_ver() to obtain these values */
-#define SMBLIB_VERSION		"2.61"      /* SMB library version */
-#define SMB_VERSION 		0x0121		/* SMB format version */
+#define SMBLIB_VERSION		"3.00"      /* SMB library version */
+#define SMB_VERSION 		0x0300		/* SMB format version */
 										/* High byte major, low byte minor */
 
 static char* nulstr="";
 
-int SMBCALL smb_ver(void)
+int smb_ver(void)
 {
 	return(SMB_VERSION);
 }
 
-char* SMBCALL smb_lib_ver(void)
+char* smb_lib_ver(void)
 {
 	return(SMBLIB_VERSION);
 }
@@ -56,7 +56,7 @@ char* SMBCALL smb_lib_ver(void)
 /* Open a message base of name 'smb->file'                                  */
 /* Opens files for READing messages or updating message indices only        */
 /****************************************************************************/
-int SMBCALL smb_open(smb_t* smb)
+int smb_open(smb_t* smb)
 {
 	int			i;
 	time_t		start=0;
@@ -100,13 +100,13 @@ int SMBCALL smb_open(smb_t* smb)
 			smb_close(smb);
 			return(SMB_ERR_READ);
 		}
-		if(memcmp(hdr.id,SMB_HEADER_ID,LEN_HEADER_ID) && !smb->continue_on_error) {
+		if(memcmp(hdr.smbhdr_id,SMB_HEADER_ID,LEN_HEADER_ID) && !smb->continue_on_error) {
 			safe_snprintf(smb->last_error,sizeof(smb->last_error)
 				,"%s corrupt SMB header ID: %02X %02X %02X %02X", __FUNCTION__
-				,hdr.id[0]
-				,hdr.id[1]
-				,hdr.id[2]
-				,hdr.id[3]
+				,hdr.smbhdr_id[0]
+				,hdr.smbhdr_id[1]
+				,hdr.smbhdr_id[2]
+				,hdr.smbhdr_id[3]
 				);
 			smb_close(smb);
 			return(SMB_ERR_HDR_ID); 
@@ -139,7 +139,7 @@ int SMBCALL smb_open(smb_t* smb)
 	return(SMB_SUCCESS);
 }
 
-int SMBCALL smb_open_index(smb_t* smb)
+int smb_open_index(smb_t* smb)
 {
 	return smb_open_fp(smb, &smb->sid_fp, SH_DENYNO);
 }
@@ -147,7 +147,7 @@ int SMBCALL smb_open_index(smb_t* smb)
 /****************************************************************************/
 /* Closes the currently open message base									*/
 /****************************************************************************/
-void SMBCALL smb_close(smb_t* smb)
+void smb_close(smb_t* smb)
 {
 	if(smb->shd_fp!=NULL) {
 		smb_unlocksmbhdr(smb);		   /* In case it's been locked */
@@ -176,7 +176,7 @@ static char* smb_lockfname(smb_t* smb, char* fname, size_t maxlen)
 /* This function is used to lock an entire message base for exclusive		*/
 /* (typically for maintenance/repair)										*/
 /****************************************************************************/
-int SMBCALL smb_lock(smb_t* smb)
+int smb_lock(smb_t* smb)
 {
 	char	path[MAX_PATH+1];
 	int		file;
@@ -199,7 +199,7 @@ int SMBCALL smb_lock(smb_t* smb)
 	return(SMB_SUCCESS);
 }
 
-int SMBCALL smb_unlock(smb_t* smb)
+int smb_unlock(smb_t* smb)
 {
 	char	path[MAX_PATH+1];
 
@@ -213,7 +213,7 @@ int SMBCALL smb_unlock(smb_t* smb)
 	return(SMB_SUCCESS);
 }
 
-BOOL SMBCALL smb_islocked(smb_t* smb)
+BOOL smb_islocked(smb_t* smb)
 {
 	char	path[MAX_PATH+1];
 
@@ -228,7 +228,7 @@ BOOL SMBCALL smb_islocked(smb_t* smb)
 /* Retries for smb.retry_time number of seconds								*/
 /* Return 0 on success, non-zero otherwise									*/
 /****************************************************************************/
-int SMBCALL smb_trunchdr(smb_t* smb)
+int smb_trunchdr(smb_t* smb)
 {
 	time_t	start=0;
 
@@ -267,7 +267,7 @@ int SMBCALL smb_trunchdr(smb_t* smb)
 /****************************************************************************/
 /* Attempts for smb.retry_time number of seconds to lock the msg base hdr	*/
 /****************************************************************************/
-int SMBCALL smb_locksmbhdr(smb_t* smb)
+int smb_locksmbhdr(smb_t* smb)
 {
 	time_t	start=0;
 
@@ -302,7 +302,7 @@ int SMBCALL smb_locksmbhdr(smb_t* smb)
 /****************************************************************************/
 /* Read the SMB header from the header file and place into smb.status		*/
 /****************************************************************************/
-int SMBCALL smb_getstatus(smb_t* smb)
+int smb_getstatus(smb_t* smb)
 {
 	int 	i;
 
@@ -329,7 +329,7 @@ int SMBCALL smb_getstatus(smb_t* smb)
 /****************************************************************************/
 /* Writes message base header												*/
 /****************************************************************************/
-int SMBCALL smb_putstatus(smb_t* smb)
+int smb_putstatus(smb_t* smb)
 {
 	int i;
 
@@ -356,7 +356,7 @@ int SMBCALL smb_putstatus(smb_t* smb)
 /****************************************************************************/
 /* Unlocks previously locked message base header 							*/
 /****************************************************************************/
-int SMBCALL smb_unlocksmbhdr(smb_t* smb)
+int smb_unlocksmbhdr(smb_t* smb)
 {
 	if(smb->locked) {
 		if(smb->shd_fp==NULL) {
@@ -380,7 +380,7 @@ int SMBCALL smb_unlocksmbhdr(smb_t* smb)
 /****************************************************************************/
 /* Is the offset a valid message header offset?								*/
 /****************************************************************************/
-BOOL SMBCALL smb_valid_hdr_offset(smb_t* smb, ulong offset)
+BOOL smb_valid_hdr_offset(smb_t* smb, ulong offset)
 {
 	if(offset<sizeof(smbhdr_t)+sizeof(smbstatus_t) 
 		|| offset<smb->status.header_offset) {
@@ -395,7 +395,7 @@ BOOL SMBCALL smb_valid_hdr_offset(smb_t* smb, ulong offset)
 /****************************************************************************/
 /* Attempts for smb.retry_time number of seconds to lock the hdr for 'msg'  */
 /****************************************************************************/
-int SMBCALL smb_lockmsghdr(smb_t* smb, smbmsg_t* msg)
+int smb_lockmsghdr(smb_t* smb, smbmsg_t* msg)
 {
 	time_t	start=0;
 
@@ -430,12 +430,13 @@ int SMBCALL smb_lockmsghdr(smb_t* smb, smbmsg_t* msg)
 /* Either msg->hdr.number or msg->offset must be initialized before 		*/
 /* calling this function													*/
 /****************************************************************************/
-int SMBCALL smb_getmsgidx(smb_t* smb, smbmsg_t* msg)
+int smb_getmsgidx(smb_t* smb, smbmsg_t* msg)
 {
 	idxrec_t	idx;
 	long		byte_offset;
 	ulong		l,total,bot,top;
-	long		length;
+	off_t		length;
+	size_t		idxreclen = smb_idxreclen(smb);
 
 	if(smb->sid_fp==NULL) {
 		safe_snprintf(smb->last_error,sizeof(smb->last_error),"%s index not open", __FUNCTION__);
@@ -444,12 +445,12 @@ int SMBCALL smb_getmsgidx(smb_t* smb, smbmsg_t* msg)
 	clearerr(smb->sid_fp);
 
 	length=filelength(fileno(smb->sid_fp));
-	if(length<(long)sizeof(idxrec_t)) {
+	if(length<(long)idxreclen) {
 		safe_snprintf(smb->last_error,sizeof(smb->last_error)
 			,"%s invalid index file length: %ld", __FUNCTION__,length);
 		return(SMB_ERR_FILE_LEN);
 	}
-	total=length/sizeof(idxrec_t);
+	total=(ulong)(length/idxreclen);
 	if(!total) {
 		safe_snprintf(smb->last_error,sizeof(smb->last_error)
 			,"%s invalid index file length: %ld", __FUNCTION__,length);
@@ -457,14 +458,14 @@ int SMBCALL smb_getmsgidx(smb_t* smb, smbmsg_t* msg)
 	}
 
 	if(!msg->hdr.number) {
-		if(msg->offset<0)
-			byte_offset=length-((-msg->offset)*sizeof(idxrec_t));
+		if(msg->idx_offset<0)
+			byte_offset=(long)(length-((-msg->idx_offset)*idxreclen));
 		else
-			byte_offset=msg->offset*sizeof(idxrec_t);
+			byte_offset=msg->idx_offset*idxreclen;
 		if(byte_offset>=length) {
 			safe_snprintf(smb->last_error,sizeof(smb->last_error)
 				,"%s invalid index offset: %ld, byte offset: %ld, length: %ld", __FUNCTION__
-				,(long)msg->offset, byte_offset, length);
+				,(long)msg->idx_offset, byte_offset, length);
 			return(SMB_ERR_HDR_OFFSET);
 		}
 		if(ftell(smb->sid_fp) != byte_offset) {
@@ -472,18 +473,18 @@ int SMBCALL smb_getmsgidx(smb_t* smb, smbmsg_t* msg)
 				safe_snprintf(smb->last_error,sizeof(smb->last_error)
 					,"%s %d '%s' seeking to offset %ld (byte %lu) in index file", __FUNCTION__
 					,get_errno(),STRERROR(get_errno())
-					,(long)msg->offset,byte_offset);
+					,(long)msg->idx_offset,byte_offset);
 				return(SMB_ERR_SEEK);
 			}
 		}
-		if(smb_fread(smb,&msg->idx,sizeof(idxrec_t),smb->sid_fp)!=sizeof(idxrec_t)) {
+		if(smb_fread(smb,&msg->idx,sizeof(msg->idx),smb->sid_fp)!=sizeof(msg->idx)) {
 			safe_snprintf(smb->last_error,sizeof(smb->last_error)
 				,"%s reading index at offset %ld (byte %lu)", __FUNCTION__
-				,(long)msg->offset,byte_offset);
+				,(long)msg->idx_offset,byte_offset);
 			return(SMB_ERR_READ);
 		}
 		/* Save the correct offset (from the beginning of the file) */
-		msg->offset=byte_offset/sizeof(idxrec_t);
+		msg->idx_offset=byte_offset/idxreclen;
 		return(SMB_SUCCESS); 
 	}
 
@@ -496,17 +497,17 @@ int SMBCALL smb_getmsgidx(smb_t* smb, smbmsg_t* msg)
 				, __FUNCTION__, (ulong)msg->hdr.number);
 			return(SMB_ERR_NOT_FOUND);
 		}
-		if(fseek(smb->sid_fp,l*sizeof(idxrec_t),SEEK_SET)) {
+		if(fseek(smb->sid_fp,l*idxreclen,SEEK_SET)) {
 			safe_snprintf(smb->last_error,sizeof(smb->last_error)
 				,"%s %d '%s' seeking to offset %lu (byte %lu) in index file", __FUNCTION__
 				,get_errno(),STRERROR(get_errno())
-				,l,l*sizeof(idxrec_t));
+				,l,l*idxreclen);
 			return(SMB_ERR_SEEK);
 		}
-		if(smb_fread(smb,&idx,sizeof(idxrec_t),smb->sid_fp)!=sizeof(idxrec_t)) {
+		if(smb_fread(smb,&idx,sizeof(idx),smb->sid_fp)!=sizeof(idx)) {
 			safe_snprintf(smb->last_error,sizeof(smb->last_error)
 				,"%s reading index at offset %lu (byte %lu)", __FUNCTION__
-				,l,l*sizeof(idxrec_t));
+				,l,l*sizeof(idx));
 			return(SMB_ERR_READ);
 		}
 		if(bot==top-1 && idx.number!=msg->hdr.number) {
@@ -527,14 +528,43 @@ int SMBCALL smb_getmsgidx(smb_t* smb, smbmsg_t* msg)
 		break; 
 	}
 	msg->idx=idx;
-	msg->offset=l;
+	msg->idx_offset=l;
 	return(SMB_SUCCESS);
 }
 
+/****************************************************************************/
+/* Count the number of msg index records with specific attribute flags		*/
+/****************************************************************************/
+uint32_t smb_count_idx_records(smb_t* smb, uint16_t mask, uint16_t cmp)
+{
+	int32_t offset = 0;
+	uint32_t count = 0;
+	for(offset = 0; ;offset++) {
+		smbmsg_t msg;
+		memset(&msg, 0, sizeof(msg));
+		msg.idx_offset = offset;
+		if(smb_getmsgidx(smb, &msg) != SMB_SUCCESS)
+			break;
+		if((msg.idx.attr & mask) == cmp)
+			count++;
+	}
+	return count;
+}
+
+/****************************************************************************/
+/* Returns the length (in bytes) of the SMB's index records					*/
+/****************************************************************************/
+size_t smb_idxreclen(smb_t* smb)
+{
+	if(smb->status.attr&SMB_FILE_DIRECTORY)
+		return sizeof(fileidxrec_t);
+	return sizeof(idxrec_t);
+}
+
 /****************************************************************************/
 /* Reads the first index record in the open message base 					*/
 /****************************************************************************/
-int SMBCALL smb_getfirstidx(smb_t* smb, idxrec_t *idx)
+int smb_getfirstidx(smb_t* smb, idxrec_t *idx)
 {
 	if(smb->sid_fp==NULL) {
 		safe_snprintf(smb->last_error,sizeof(smb->last_error),"%s index not open", __FUNCTION__);
@@ -547,7 +577,7 @@ int SMBCALL smb_getfirstidx(smb_t* smb, idxrec_t *idx)
 			,get_errno(),STRERROR(get_errno()));
 		return(SMB_ERR_SEEK);
 	}
-	if(smb_fread(smb,idx,sizeof(idxrec_t),smb->sid_fp)!=sizeof(idxrec_t)) {
+	if(smb_fread(smb,idx,sizeof(*idx),smb->sid_fp)!=sizeof(*idx)) {
 		safe_snprintf(smb->last_error,sizeof(smb->last_error)
 			,"%s reading first index", __FUNCTION__);
 		return(SMB_ERR_READ);
@@ -558,9 +588,10 @@ int SMBCALL smb_getfirstidx(smb_t* smb, idxrec_t *idx)
 /****************************************************************************/
 /* Reads the last index record in the open message base 					*/
 /****************************************************************************/
-int SMBCALL smb_getlastidx(smb_t* smb, idxrec_t *idx)
+int smb_getlastidx(smb_t* smb, idxrec_t *idx)
 {
-	long length;
+	off_t length;
+	size_t idxreclen = smb_idxreclen(smb);
 
 	if(smb->sid_fp==NULL) {
 		safe_snprintf(smb->last_error,sizeof(smb->last_error),"%s index not open", __FUNCTION__);
@@ -568,19 +599,19 @@ int SMBCALL smb_getlastidx(smb_t* smb, idxrec_t *idx)
 	}
 	clearerr(smb->sid_fp);
 	length=filelength(fileno(smb->sid_fp));
-	if(length<(long)sizeof(idxrec_t)) {
+	if(length<(long)idxreclen) {
 		safe_snprintf(smb->last_error,sizeof(smb->last_error)
 			,"%s invalid index file length: %ld", __FUNCTION__,length);
 		return(SMB_ERR_FILE_LEN);
 	}
-	if(fseek(smb->sid_fp,length-sizeof(idxrec_t),SEEK_SET)) {
+	if(fseeko(smb->sid_fp,length-idxreclen,SEEK_SET)) {
 		safe_snprintf(smb->last_error,sizeof(smb->last_error)
 			,"%s %d '%s' seeking to %u in index file", __FUNCTION__
 			,get_errno(),STRERROR(get_errno())
 			,(unsigned)(length-sizeof(idxrec_t)));
 		return(SMB_ERR_SEEK);
 	}
-	if(smb_fread(smb,idx,sizeof(idxrec_t),smb->sid_fp)!=sizeof(idxrec_t)) {
+	if(smb_fread(smb,idx,sizeof(*idx),smb->sid_fp)!=sizeof(*idx)) {
 		safe_snprintf(smb->last_error,sizeof(smb->last_error)
 			,"%s reading last index", __FUNCTION__);
 		return(SMB_ERR_READ);
@@ -594,12 +625,13 @@ int SMBCALL smb_getlastidx(smb_t* smb, idxrec_t *idx)
 /* must call smb_locksmbhdr() before, smb_unlocksmbhdr() after.				*/
 /* Returns >= 0 on success, negative (SMB_* error) on failure.				*/
 /****************************************************************************/
-long SMBCALL smb_getmsgidx_by_time(smb_t* smb, idxrec_t* match, time_t t)
+long smb_getmsgidx_by_time(smb_t* smb, idxrec_t* match, time_t t)
 {
     int			result;
 	long		match_offset;
 	ulong		total, bot, top;
 	idxrec_t	idx;
+	size_t		idxreclen = smb_idxreclen(smb);
 
 	if(match == NULL)
 		return SMB_BAD_PARAMETER;
@@ -609,7 +641,7 @@ long SMBCALL smb_getmsgidx_by_time(smb_t* smb, idxrec_t* match, time_t t)
 	if(t <= 0)
 		return SMB_BAD_PARAMETER;
 
-	total = filelength(fileno(smb->sid_fp))/sizeof(idxrec_t);
+	total = (ulong)(filelength(fileno(smb->sid_fp))/idxreclen);
 
 	if(!total)	/* Empty base */
 		return SMB_ERR_NOT_FOUND;
@@ -629,9 +661,9 @@ long SMBCALL smb_getmsgidx_by_time(smb_t* smb, idxrec_t* match, time_t t)
 	clearerr(smb->sid_fp);
 	while(bot <= top) {
 		long idx_offset = (bot + top) / 2;
-		if(fseek(smb->sid_fp, idx_offset * sizeof(idxrec_t), SEEK_SET) != 0)
+		if(fseek(smb->sid_fp, idx_offset * idxreclen, SEEK_SET) != 0)
 			return SMB_ERR_SEEK;
-		if(fread(&idx, 1, sizeof(idx), smb->sid_fp) != sizeof(idxrec_t))
+		if(fread(&idx, 1, sizeof(idx), smb->sid_fp) != sizeof(idx))
 			return SMB_ERR_READ;
 		if((time_t)idx.time < t) {
 			bot = idx_offset + 1;
@@ -653,7 +685,7 @@ long SMBCALL smb_getmsgidx_by_time(smb_t* smb, idxrec_t* match, time_t t)
 /* Figures out the total length of the header record for 'msg'              */
 /* Returns length 															*/
 /****************************************************************************/
-ulong SMBCALL smb_getmsghdrlen(smbmsg_t* msg)
+ulong smb_getmsghdrlen(smbmsg_t* msg)
 {
 	int i;
 	ulong length;
@@ -674,7 +706,7 @@ ulong SMBCALL smb_getmsghdrlen(smbmsg_t* msg)
 /* Figures out the total length of the data buffer for 'msg'                */
 /* Returns length															*/
 /****************************************************************************/
-ulong SMBCALL smb_getmsgdatlen(smbmsg_t* msg)
+ulong smb_getmsgdatlen(smbmsg_t* msg)
 {
 	int i;
 	ulong length=0L;
@@ -688,7 +720,7 @@ ulong SMBCALL smb_getmsgdatlen(smbmsg_t* msg)
 /* Figures out the total length of the text buffer for 'msg'                */
 /* Returns length															*/
 /****************************************************************************/
-ulong SMBCALL smb_getmsgtxtlen(smbmsg_t* msg)
+ulong smb_getmsgtxtlen(smbmsg_t* msg)
 {
 	int i;
 	ulong length=0L;
@@ -922,12 +954,12 @@ static void clear_convenience_ptrs(smbmsg_t* msg)
 /* Must call smb_freemsgmem() to free memory allocated for var len strs 	*/
 /* Returns 0 on success, non-zero if error									*/
 /****************************************************************************/
-int SMBCALL smb_getmsghdr(smb_t* smb, smbmsg_t* msg)
+int smb_getmsghdr(smb_t* smb, smbmsg_t* msg)
 {
 	void	*vp,**vpp;
 	size_t	i;
 	long	l,offset;
-	idxrec_t idx;
+	fileidxrec_t idx;
 
 	if(smb->shd_fp==NULL) {
 		safe_snprintf(smb->last_error,sizeof(smb->last_error),"%s msgbase not open", __FUNCTION__);
@@ -941,37 +973,37 @@ int SMBCALL smb_getmsghdr(smb_t* smb, smbmsg_t* msg)
 	if(offset != msg->idx.offset) {
 		if(fseek(smb->shd_fp,msg->idx.offset,SEEK_SET) != 0) {
 			safe_snprintf(smb->last_error,sizeof(smb->last_error)
-				,"%s %d '%s' seeking to %lu in header file", __FUNCTION__
+				,"%s %d '%s' seeking to offset %lu in header file", __FUNCTION__
 				,get_errno(),STRERROR(get_errno())
 				,(ulong)msg->idx.offset);
 			return(SMB_ERR_SEEK);
 		}
 	}
 
-	idx=msg->idx;
-	offset=msg->offset;
+	idx = msg->file_idx;
+	offset=msg->idx_offset;
 	memset(msg,0,sizeof(smbmsg_t));
-	msg->idx=idx;
-	msg->offset=offset;
+	msg->file_idx = idx;
+	msg->idx_offset=offset;
 	if(smb_fread(smb,&msg->hdr,sizeof(msghdr_t),smb->shd_fp)!=sizeof(msghdr_t)) {
 		safe_snprintf(smb->last_error,sizeof(smb->last_error)
-			,"%s reading msg header", __FUNCTION__);
+			,"%s reading msg header at offset %lu", __FUNCTION__, (ulong)msg->idx.offset);
 		return(SMB_ERR_READ);
 	}
-	if(memcmp(msg->hdr.id,SHD_HEADER_ID,LEN_HEADER_ID)) {
+	if(memcmp(msg->hdr.msghdr_id,SHD_HEADER_ID,LEN_HEADER_ID)) {
 		safe_snprintf(smb->last_error,sizeof(smb->last_error)
 			,"%s corrupt message header ID (%02X %02X %02X %02X) at offset %lu", __FUNCTION__
-			,msg->hdr.id[0]
-			,msg->hdr.id[1]
-			,msg->hdr.id[2]
-			,msg->hdr.id[3]
+			,msg->hdr.msghdr_id[0]
+			,msg->hdr.msghdr_id[1]
+			,msg->hdr.msghdr_id[2]
+			,msg->hdr.msghdr_id[3]
 			,(ulong)msg->idx.offset);
 		return(SMB_ERR_HDR_ID);
 	}
 	if(msg->hdr.version<0x110) {
 		safe_snprintf(smb->last_error,sizeof(smb->last_error)
-			,"%s insufficient header version: %X", __FUNCTION__
-			,msg->hdr.version);
+			,"%s insufficient header version: %X at offset %lu", __FUNCTION__
+			,msg->hdr.version, (ulong)msg->idx.offset);
 		return(SMB_ERR_HDR_VER);
 	}
 	l=sizeof(msg->hdr);
@@ -1059,7 +1091,7 @@ int SMBCALL smb_getmsghdr(smb_t* smb, smbmsg_t* msg)
 /****************************************************************************/
 /* Frees memory allocated for variable-length header fields in 'msg'        */
 /****************************************************************************/
-void SMBCALL smb_freemsghdrmem(smbmsg_t* msg)
+void smb_freemsghdrmem(smbmsg_t* msg)
 {
 	uint16_t	i;
 
@@ -1083,7 +1115,7 @@ void SMBCALL smb_freemsghdrmem(smbmsg_t* msg)
 /****************************************************************************/
 /* Frees memory allocated for 'msg'                                         */
 /****************************************************************************/
-void SMBCALL smb_freemsgmem(smbmsg_t* msg)
+void smb_freemsgmem(smbmsg_t* msg)
 {
 	if(msg->dfield) {
 		free(msg->dfield);
@@ -1093,12 +1125,16 @@ void SMBCALL smb_freemsgmem(smbmsg_t* msg)
 	FREE_AND_NULL(msg->text_subtype);
 	FREE_AND_NULL(msg->text_charset);
 	smb_freemsghdrmem(msg);
+	if(msg->text != NULL) {
+		free(msg->text);
+		msg->text = NULL;
+	}
 }
 
 /****************************************************************************/
 /* Copies memory allocated for 'srcmsg' to 'msg'							*/
 /****************************************************************************/
-int SMBCALL smb_copymsgmem(smb_t* smb, smbmsg_t* msg, smbmsg_t* srcmsg)
+int smb_copymsgmem(smb_t* smb, smbmsg_t* msg, smbmsg_t* srcmsg)
 {
 	int i;
 
@@ -1157,7 +1193,7 @@ int SMBCALL smb_copymsgmem(smb_t* smb, smbmsg_t* msg, smbmsg_t* srcmsg)
 /****************************************************************************/
 /* Unlocks header for 'msg'                                                 */
 /****************************************************************************/
-int SMBCALL smb_unlockmsghdr(smb_t* smb, smbmsg_t* msg)
+int smb_unlockmsghdr(smb_t* smb, smbmsg_t* msg)
 {
 	if(smb->shd_fp==NULL) {
 		safe_snprintf(smb->last_error,sizeof(smb->last_error),"%s msgbase not open", __FUNCTION__);
@@ -1172,7 +1208,7 @@ int SMBCALL smb_unlockmsghdr(smb_t* smb, smbmsg_t* msg)
 /****************************************************************************/
 /* Adds a header field to the 'msg' structure (in memory only)              */
 /****************************************************************************/
-int SMBCALL smb_hfield_add(smbmsg_t* msg, uint16_t type, size_t length, void* data, BOOL insert)
+int smb_hfield_add(smbmsg_t* msg, uint16_t type, size_t length, void* data, BOOL insert)
 {
 	void**		vpp;
 	hfield_t*	hp;
@@ -1210,7 +1246,7 @@ int SMBCALL smb_hfield_add(smbmsg_t* msg, uint16_t type, size_t length, void* da
 /****************************************************************************/
 /* Adds a list of header fields to the 'msg' structure (in memory only)     */
 /****************************************************************************/
-int	SMBCALL smb_hfield_add_list(smbmsg_t* msg, hfield_t** hfield_list, void** hfield_dat, BOOL insert)
+int	smb_hfield_add_list(smbmsg_t* msg, hfield_t** hfield_list, void** hfield_dat, BOOL insert)
 {
 	int			retval;
 	unsigned	n;
@@ -1229,7 +1265,7 @@ int	SMBCALL smb_hfield_add_list(smbmsg_t* msg, hfield_t** hfield_list, void** hf
 /****************************************************************************/
 /* Convenience function to add an ASCIIZ string header field (or blank)		*/
 /****************************************************************************/
-int SMBCALL smb_hfield_add_str(smbmsg_t* msg, uint16_t type, const char* str, BOOL insert)
+int smb_hfield_add_str(smbmsg_t* msg, uint16_t type, const char* str, BOOL insert)
 {
 	return smb_hfield_add(msg, type, str==NULL ? 0:strlen(str), (void*)str, insert);
 }
@@ -1237,7 +1273,7 @@ int SMBCALL smb_hfield_add_str(smbmsg_t* msg, uint16_t type, const char* str, BO
 /****************************************************************************/
 /* Convenience function to add an ASCIIZ string header field (NULL ignored)	*/
 /****************************************************************************/
-int SMBCALL smb_hfield_string(smbmsg_t* msg, uint16_t type, const char* str)
+int smb_hfield_string(smbmsg_t* msg, uint16_t type, const char* str)
 {
 	if(str == NULL)
 		return SMB_ERR_HDR_FIELD;
@@ -1249,7 +1285,7 @@ int SMBCALL smb_hfield_string(smbmsg_t* msg, uint16_t type, const char* str)
 /* Pass NULL for net_type to have the auto-detected net_type hfield	added	*/
 /* as well.																	*/
 /****************************************************************************/
-int	SMBCALL smb_hfield_add_netaddr(smbmsg_t* msg, uint16_t type, const char* addr, uint16_t* net_type, BOOL insert)
+int	smb_hfield_add_netaddr(smbmsg_t* msg, uint16_t type, const char* addr, uint16_t* net_type, BOOL insert)
 {
 	int			result;
 	fidoaddr_t	sys_addr = {0,0,0,0};	/* replace unspecified fields with 0 (don't assume 1:1/1) */
@@ -1292,7 +1328,7 @@ int	SMBCALL smb_hfield_add_netaddr(smbmsg_t* msg, uint16_t type, const char* add
 /****************************************************************************/
 /* Appends data to an existing header field (in memory only)				*/
 /****************************************************************************/
-int SMBCALL smb_hfield_append(smbmsg_t* msg, uint16_t type, size_t length, void* data)
+int smb_hfield_append(smbmsg_t* msg, uint16_t type, size_t length, void* data)
 {
 	int		i;
 	BYTE*	p;
@@ -1329,7 +1365,7 @@ int SMBCALL smb_hfield_append(smbmsg_t* msg, uint16_t type, size_t length, void*
 /****************************************************************************/
 /* Appends data to an existing ASCIIZ header field (in memory only)			*/
 /****************************************************************************/
-int SMBCALL smb_hfield_append_str(smbmsg_t* msg, uint16_t type, const char* str)
+int smb_hfield_append_str(smbmsg_t* msg, uint16_t type, const char* str)
 {
 	return smb_hfield_append(msg, type, str==NULL ? 0:strlen(str), (void*)str);
 }
@@ -1337,7 +1373,7 @@ int SMBCALL smb_hfield_append_str(smbmsg_t* msg, uint16_t type, const char* str)
 /****************************************************************************/
 /* Replaces an header field value (in memory only)							*/
 /****************************************************************************/
-int SMBCALL smb_hfield_replace(smbmsg_t* msg, uint16_t type, size_t length, void* data)
+int smb_hfield_replace(smbmsg_t* msg, uint16_t type, size_t length, void* data)
 {
 	int		i;
 	void*	p;
@@ -1367,7 +1403,7 @@ int SMBCALL smb_hfield_replace(smbmsg_t* msg, uint16_t type, size_t length, void
 /****************************************************************************/
 /* Replace an existing ASCIIZ header field value (in memory only)			*/
 /****************************************************************************/
-int SMBCALL smb_hfield_replace_str(smbmsg_t* msg, uint16_t type, const char* str)
+int smb_hfield_replace_str(smbmsg_t* msg, uint16_t type, const char* str)
 {
 	return smb_hfield_replace(msg, type, str==NULL ? 0:strlen(str), (void*)str);
 }
@@ -1375,7 +1411,7 @@ int SMBCALL smb_hfield_replace_str(smbmsg_t* msg, uint16_t type, const char* str
 /****************************************************************************/
 /* Searches for a specific header field (by type) and returns it			*/
 /****************************************************************************/
-void* SMBCALL smb_get_hfield(smbmsg_t* msg, uint16_t type, hfield_t** hfield)
+void* smb_get_hfield(smbmsg_t* msg, uint16_t type, hfield_t** hfield)
 {
 	int i;
 
@@ -1389,12 +1425,31 @@ void* SMBCALL smb_get_hfield(smbmsg_t* msg, uint16_t type, hfield_t** hfield)
 	return(NULL);
 }
 
+/****************************************************************************/
+/* Add or replace a specific header field (by type)							*/
+/****************************************************************************/
+int smb_new_hfield(smbmsg_t* msg, uint16_t type, size_t length, void* data)
+{
+	if(smb_get_hfield(msg, type, NULL))
+		return smb_hfield_replace(msg, type, length, data);
+	else
+		return smb_hfield_add(msg, type, length, data, /* insert */FALSE);
+}
+
+/****************************************************************************/
+/* Add or replace a specific string header field (by type)					*/
+/****************************************************************************/
+int smb_new_hfield_str(smbmsg_t* msg, uint16_t type, const char* str)
+{
+	return smb_new_hfield(msg, type, str == NULL ? 0 : strlen(str), (void*)str);
+}
+
 /****************************************************************************/
 /* Adds a data field to the 'msg' structure (in memory only)                */
 /* Automatically figures out the offset into the data buffer from existing	*/
 /* dfield lengths															*/
 /****************************************************************************/
-int SMBCALL smb_dfield(smbmsg_t* msg, uint16_t type, ulong length)
+int smb_dfield(smbmsg_t* msg, uint16_t type, ulong length)
 {
 	dfield_t* dp;
 	int i,j;
@@ -1416,12 +1471,12 @@ int SMBCALL smb_dfield(smbmsg_t* msg, uint16_t type, ulong length)
 /* Checks CRC history file for duplicate crc. If found, returns SMB_DUPE_MSG*/
 /* If no dupe, adds to CRC history and returns 0, or negative if error. 	*/
 /****************************************************************************/
-int SMBCALL smb_addcrc(smb_t* smb, uint32_t crc)
+int smb_addcrc(smb_t* smb, uint32_t crc)
 {
 	char	str[MAX_PATH+1];
 	int 	file;
 	int		wr;
-	long	length;
+	off_t	length;
 	long	newlen;
 	ulong	l;
 	uint32_t *buf;
@@ -1461,7 +1516,7 @@ int SMBCALL smb_addcrc(smb_t* smb, uint32_t crc)
 	}
 
 	if(length!=0) {
-		if((buf=(uint32_t*)malloc(length))==NULL) {
+		if((buf=(uint32_t*)malloc((size_t)length))==NULL) {
 			close(file);
 			safe_snprintf(smb->last_error,sizeof(smb->last_error)
 				,"%s malloc failure of %ld bytes", __FUNCTION__
@@ -1469,7 +1524,7 @@ int SMBCALL smb_addcrc(smb_t* smb, uint32_t crc)
 			return(SMB_ERR_MEM); 
 		}
 
-		if(read(file,buf,length)!=length) {
+		if(read(file,buf,(uint)length)!=length) {
 			close(file);
 			free(buf);
 			safe_snprintf(smb->last_error,sizeof(smb->last_error)
@@ -1517,12 +1572,13 @@ int SMBCALL smb_addcrc(smb_t* smb, uint32_t crc)
 /* If storage is SMB_HYPERALLOC, no allocation tables are used (fastest)	*/
 /* This function will UN-lock the SMB header								*/
 /****************************************************************************/
-int SMBCALL smb_addmsghdr(smb_t* smb, smbmsg_t* msg, int storage)
+int smb_addmsghdr(smb_t* smb, smbmsg_t* msg, int storage)
 {
 	int		i;
-	long	l;
+	off_t	l;
 	ulong	hdrlen;
-	long	idxlen;
+	off_t	idxlen;
+	size_t	idxreclen = smb_idxreclen(smb);
 
 	if(smb->shd_fp==NULL) {
 		safe_snprintf(smb->last_error,sizeof(smb->last_error),"%s msgbase not open", __FUNCTION__);
@@ -1547,10 +1603,10 @@ int SMBCALL smb_addmsghdr(smb_t* smb, smbmsg_t* msg, int storage)
 	}
 
 	idxlen = filelength(fileno(smb->sid_fp));
-	if(idxlen != (smb->status.total_msgs * sizeof(idxrec_t))) {
+	if(idxlen != (smb->status.total_msgs * idxreclen)) {
 		safe_snprintf(smb->last_error, sizeof(smb->last_error)
-			,"%s index file length (%ld) unexpected (%ld)", __FUNCTION__
-			,idxlen, (long)(smb->status.total_msgs * sizeof(idxrec_t)));
+			,"%s index file length (%ld), expected (%ld)", __FUNCTION__
+			,idxlen, smb->status.total_msgs * idxreclen);
 		smb_unlocksmbhdr(smb);
 		return SMB_ERR_FILE_LEN;
 	}
@@ -1585,11 +1641,11 @@ int SMBCALL smb_addmsghdr(smb_t* smb, smbmsg_t* msg, int storage)
 		smb_close_ha(smb);
 	if(l<0) {
 		smb_unlocksmbhdr(smb);
-		return(l); 
+		return (int)l; 
 	}
 
-	msg->idx.offset=smb->status.header_offset+l;
-	msg->offset=smb->status.total_msgs;
+	msg->idx.offset=(uint32_t)(smb->status.header_offset + l);
+	msg->idx_offset=smb->status.total_msgs;
 	i=smb_putmsg(smb,msg);
 	if(i==SMB_SUCCESS) {
 		smb->status.last_msg++;
@@ -1605,7 +1661,7 @@ int SMBCALL smb_addmsghdr(smb_t* smb, smbmsg_t* msg, int storage)
 /* Nothing should be locked prior to calling this function and nothing		*/
 /* should (normally) be locked when it exits								*/
 /****************************************************************************/
-int SMBCALL smb_updatemsg(smb_t* smb, smbmsg_t* msg)
+int smb_updatemsg(smb_t* smb, smbmsg_t* msg)
 {
 	int retval;
 
@@ -1627,7 +1683,7 @@ int SMBCALL smb_updatemsg(smb_t* smb, smbmsg_t* msg)
 /****************************************************************************/
 /* Writes both header and index information for msg 'msg'                   */
 /****************************************************************************/
-int SMBCALL smb_putmsg(smb_t* smb, smbmsg_t* msg)
+int smb_putmsg(smb_t* smb, smbmsg_t* msg)
 {
 	int i;
 
@@ -1643,27 +1699,32 @@ int SMBCALL smb_putmsg(smb_t* smb, smbmsg_t* msg)
 /* Initializes/re-synchronizes all the fields of the message index record	*/
 /* with the values from the message header (except for the header offset)	*/
 /****************************************************************************/
-int SMBCALL smb_init_idx(smb_t* smb, smbmsg_t* msg)
+int smb_init_idx(smb_t* smb, smbmsg_t* msg)
 {
-	msg->idx.subj=smb_subject_crc(msg->subj);
-
-	if(smb->status.attr&SMB_EMAIL) {
-		if(msg->to_ext)
-			msg->idx.to=atoi(msg->to_ext);
-		else
-			msg->idx.to=0;
-		if(msg->from_ext)
-			msg->idx.from=atoi(msg->from_ext);
-		else
-			msg->idx.from=0; 
-	} else if(msg->hdr.type == SMB_MSG_TYPE_BALLOT) {
+	if(msg->hdr.type == SMB_MSG_TYPE_BALLOT) {
 		msg->idx.votes = msg->hdr.votes;
 		msg->idx.remsg = msg->hdr.thread_back;
+	} else if(msg->hdr.type == SMB_MSG_TYPE_FILE) {
+		if(msg->name != NULL)
+			smb_fileidxname(msg->name, msg->file_idx.name, sizeof(msg->file_idx.name));
+		if(msg->size > 0)
+			msg->idx.size = (uint32_t)msg->size;
 	} else {
-		msg->idx.to=smb_name_crc(msg->to);
-		msg->idx.from=smb_name_crc(msg->from);
+		msg->idx.subj = smb_subject_crc(msg->subj);
+		if(smb->status.attr & SMB_EMAIL) {
+			if(msg->to_ext)
+				msg->idx.to = atoi(msg->to_ext);
+			else
+				msg->idx.to = 0;
+			if(msg->from_ext)
+				msg->idx.from = atoi(msg->from_ext);
+			else
+				msg->idx.from = 0; 
+		} else {
+			msg->idx.to = smb_name_crc(msg->to);
+			msg->idx.from = smb_name_crc(msg->from);
+		}
 	}
-
 	/* Make sure these index/header fields are always *nsync */
 	msg->idx.number	= msg->hdr.number;
 	msg->idx.attr	= msg->hdr.attr;	
@@ -1672,7 +1733,7 @@ int SMBCALL smb_init_idx(smb_t* smb, smbmsg_t* msg)
 	return(SMB_SUCCESS);
 }
 
-BOOL SMBCALL smb_msg_is_from(smbmsg_t* msg, const char* name, enum smb_net_type net_type, const void* net_addr)
+BOOL smb_msg_is_from(smbmsg_t* msg, const char* name, enum smb_net_type net_type, const void* net_addr)
 {
 	if(stricmp(msg->from, name) != 0)
 		return FALSE;
@@ -1690,7 +1751,7 @@ BOOL SMBCALL smb_msg_is_from(smbmsg_t* msg, const char* name, enum smb_net_type
 	}
 }
 
-BOOL SMBCALL smb_msg_is_utf8(const smbmsg_t* msg)
+BOOL smb_msg_is_utf8(const smbmsg_t* msg)
 {
 	for(int i=0; i < msg->total_hfields; i++) {
 		switch(msg->hfield[i].type) {
@@ -1704,7 +1765,7 @@ BOOL SMBCALL smb_msg_is_utf8(const smbmsg_t* msg)
 	return msg->text_charset != NULL && stricmp(msg->text_charset, "utf-8") == 0;
 }
 
-uint16_t SMBCALL smb_voted_already(smb_t* smb, uint32_t msgnum, const char* name, enum smb_net_type net_type, void* net_addr)
+uint16_t smb_voted_already(smb_t* smb, uint32_t msgnum, const char* name, enum smb_net_type net_type, void* net_addr)
 {
 	uint16_t votes = 0;
 	smbmsg_t msg = {0};
@@ -1720,6 +1781,7 @@ uint16_t SMBCALL smb_voted_already(smb_t* smb, uint32_t msgnum, const char* name
 			,get_errno(), STRERROR(get_errno()));
 		return SMB_ERR_SEEK;
 	}
+	memset(&msg, 0, sizeof(msg));
 	while(!votes && smb_fread(smb, &msg.idx, sizeof(msg.idx), smb->sid_fp) == sizeof(msg.idx)) {
 		if(!(msg.idx.attr&MSG_VOTE) || msg.idx.attr&MSG_POLL)
 			continue;
@@ -1751,9 +1813,10 @@ uint16_t SMBCALL smb_voted_already(smb_t* smb, uint32_t msgnum, const char* name
 /* and msg->offset must be set prior to calling to this function			*/
 /* Returns 0 if everything ok                                               */
 /****************************************************************************/
-int SMBCALL smb_putmsgidx(smb_t* smb, smbmsg_t* msg)
+int smb_putmsgidx(smb_t* smb, smbmsg_t* msg)
 {
-	long length;
+	off_t length;
+	size_t	idxreclen = smb_idxreclen(smb);
 
 	if(smb->sid_fp==NULL) {
 		safe_snprintf(smb->last_error,sizeof(smb->last_error),"%s index not open", __FUNCTION__);
@@ -1761,23 +1824,32 @@ int SMBCALL smb_putmsgidx(smb_t* smb, smbmsg_t* msg)
 	}
 	clearerr(smb->sid_fp);
 	length = filelength(fileno(smb->sid_fp));
-	if(length < (long)(msg->offset*sizeof(idxrec_t))) {
+	if(length < (long)(msg->idx_offset*idxreclen)) {
 		safe_snprintf(smb->last_error,sizeof(smb->last_error)
-			,"%s invalid index offset: %ld, byte offset: %ld, length: %lu", __FUNCTION__
-			,(long)msg->offset, (long)(msg->offset*sizeof(idxrec_t)), length);
+			,"%s invalid index offset: %ld, byte offset: %lu, length: %lu", __FUNCTION__
+			,(long)msg->idx_offset, msg->idx_offset*idxreclen, length);
 		return(SMB_ERR_HDR_OFFSET);
 	}
-	if(fseek(smb->sid_fp,msg->offset*sizeof(idxrec_t),SEEK_SET)) {
+	if(fseek(smb->sid_fp,msg->idx_offset*idxreclen,SEEK_SET)) {
 		safe_snprintf(smb->last_error,sizeof(smb->last_error)
 			,"%s %d '%s' seeking to %u in index file", __FUNCTION__
 			,get_errno(),STRERROR(get_errno())
-			,(unsigned)(msg->offset*sizeof(idxrec_t)));
+			,(unsigned)(msg->idx_offset*idxreclen));
 		return(SMB_ERR_SEEK);
 	}
-	if(!fwrite(&msg->idx,sizeof(idxrec_t),1,smb->sid_fp)) {
-		safe_snprintf(smb->last_error,sizeof(smb->last_error)
-			,"%s writing index", __FUNCTION__);
-		return(SMB_ERR_WRITE);
+	if(smb->status.attr & SMB_FILE_DIRECTORY) {
+		if(!fwrite(&msg->file_idx, sizeof(msg->file_idx), 1, smb->sid_fp)) {
+			safe_snprintf(smb->last_error,sizeof(smb->last_error)
+				,"%s %d '%s' writing index", __FUNCTION__
+				,get_errno(),STRERROR(get_errno()));
+			return(SMB_ERR_WRITE);
+		}
+	} else {
+		if(!fwrite(&msg->idx,sizeof(msg->idx),1,smb->sid_fp)) {
+			safe_snprintf(smb->last_error,sizeof(smb->last_error)
+				,"%s writing index", __FUNCTION__);
+			return(SMB_ERR_WRITE);
+		}
 	}
 	return fflush(smb->sid_fp);	/* SMB_SUCCESS == 0 */
 }
@@ -1789,7 +1861,7 @@ int SMBCALL smb_putmsgidx(smb_t* smb, smbmsg_t* msg)
 /* and msg->offset must be set prior to calling to this function			*/
 /* Returns 0 if everything ok                                               */
 /****************************************************************************/
-int SMBCALL smb_putmsghdr(smb_t* smb, smbmsg_t* msg)
+int smb_putmsghdr(smb_t* smb, smbmsg_t* msg)
 {
 	uint16_t	i;
 	ulong	hdrlen;
@@ -1829,7 +1901,7 @@ int SMBCALL smb_putmsghdr(smb_t* smb, smbmsg_t* msg)
 	/**********************************/
 	/* Set the message header ID here */
 	/**********************************/
-	memcpy(&msg->hdr.id,SHD_HEADER_ID,LEN_HEADER_ID);
+	memcpy(&msg->hdr.msghdr_id,SHD_HEADER_ID,LEN_HEADER_ID);
 
 	/************************************************/
 	/* Write the fixed portion of the header record */
@@ -1880,7 +1952,7 @@ int SMBCALL smb_putmsghdr(smb_t* smb, smbmsg_t* msg)
 /****************************************************************************/
 /* Initializes a message base's header (SMBHDR) record 						*/
 /****************************************************************************/
-int SMBCALL smb_initsmbhdr(smb_t* smb)
+int smb_initsmbhdr(smb_t* smb)
 {
 	smbhdr_t	hdr;
 
@@ -1892,7 +1964,7 @@ int SMBCALL smb_initsmbhdr(smb_t* smb)
 		&& smb_locksmbhdr(smb)!=SMB_SUCCESS)  /* header exists, so lock it */
 		return(SMB_ERR_LOCK);
 	memset(&hdr,0,sizeof(smbhdr_t));
-	memcpy(hdr.id,SMB_HEADER_ID,LEN_HEADER_ID);
+	memcpy(hdr.smbhdr_id,SMB_HEADER_ID,LEN_HEADER_ID);
 	hdr.version=SMB_VERSION;
 	hdr.length=sizeof(smbhdr_t)+sizeof(smbstatus_t);
 	smb->status.last_msg=smb->status.total_msgs=0;
@@ -1910,7 +1982,7 @@ int SMBCALL smb_initsmbhdr(smb_t* smb)
 /* Creates a sub-board's initial header file                                */
 /* Truncates and deletes other associated SMB files 						*/
 /****************************************************************************/
-int SMBCALL smb_create(smb_t* smb)
+int smb_create(smb_t* smb)
 {
     char        str[MAX_PATH+1];
 	FILE*		fp;
@@ -1948,11 +2020,11 @@ int SMBCALL smb_create(smb_t* smb)
 /****************************************************************************/
 /* Returns number of data blocks required to store "length" amount of data  */
 /****************************************************************************/
-ulong SMBCALL smb_datblocks(ulong length)
+ulong smb_datblocks(off_t length)
 {
 	ulong blocks;
 
-	blocks=length/SDT_BLOCK_LEN;
+	blocks=(ulong)(length / SDT_BLOCK_LEN);
 	if(length%SDT_BLOCK_LEN)
 		blocks++;
 	return(blocks);
@@ -1961,7 +2033,7 @@ ulong SMBCALL smb_datblocks(ulong length)
 /****************************************************************************/
 /* Returns number of header blocks required to store "length" size header   */
 /****************************************************************************/
-ulong SMBCALL smb_hdrblocks(ulong length)
+ulong smb_hdrblocks(ulong length)
 {
 	ulong blocks;
 
@@ -1974,7 +2046,7 @@ ulong SMBCALL smb_hdrblocks(ulong length)
 /****************************************************************************/
 /* Returns difference from specified timezone and UTC/GMT (in minutes)		*/
 /****************************************************************************/
-int SMBCALL smb_tzutc(int16_t zone)
+int smb_tzutc(int16_t zone)
 {
 	int tz;
 
@@ -1995,14 +2067,14 @@ int SMBCALL smb_tzutc(int16_t zone)
 /****************************************************************************/
 /* The caller needs to call smb_unlockmsghdr(smb,remsg)						*/
 /****************************************************************************/
-int SMBCALL smb_updatethread(smb_t* smb, smbmsg_t* remsg, ulong newmsgnum)
+int smb_updatethread(smb_t* smb, smbmsg_t* remsg, ulong newmsgnum)
 {
 	int			retval=SMB_ERR_NOT_FOUND;
 	ulong		nextmsgnum;
 	smbmsg_t	nextmsg;
 
  	if(!remsg->hdr.thread_first) {	/* New msg is first reply */
-		if((remsg->offset==0 || remsg->idx.offset==0)		/* index not read? */
+		if((remsg->idx_offset==0 || remsg->idx.offset==0)		/* index not read? */
 			&& (retval=smb_getmsgidx(smb,remsg))!=SMB_SUCCESS)
 			return(retval);
 		if(!remsg->hdr.length) {	/* header not read? */
@@ -2049,7 +2121,7 @@ int SMBCALL smb_updatethread(smb_t* smb, smbmsg_t* remsg, ulong newmsgnum)
 }
 
 /* Find oldest *existing* message in the thread referenced by the passed msg */
-uint32_t SMBCALL smb_first_in_thread(smb_t* smb, smbmsg_t* remsg, msghdr_t* hdr)
+uint32_t smb_first_in_thread(smb_t* smb, smbmsg_t* remsg, msghdr_t* hdr)
 {
 	smbmsg_t msg;
 
@@ -2088,7 +2160,7 @@ uint32_t SMBCALL smb_first_in_thread(smb_t* smb, smbmsg_t* remsg, msghdr_t* hdr)
 	return msgnum;
 }
 
-uint32_t SMBCALL smb_next_in_thread(smb_t* smb, smbmsg_t* remsg, msghdr_t* hdr)
+uint32_t smb_next_in_thread(smb_t* smb, smbmsg_t* remsg, msghdr_t* hdr)
 {
 	smbmsg_t msg;
 
@@ -2107,7 +2179,7 @@ uint32_t SMBCALL smb_next_in_thread(smb_t* smb, smbmsg_t* remsg, msghdr_t* hdr)
 }
 
 /* Last in this context does not mean newest */
-uint32_t SMBCALL smb_last_in_branch(smb_t* smb, smbmsg_t* remsg)
+uint32_t smb_last_in_branch(smb_t* smb, smbmsg_t* remsg)
 {
 	smbmsg_t msg;
 
@@ -2125,7 +2197,7 @@ uint32_t SMBCALL smb_last_in_branch(smb_t* smb, smbmsg_t* remsg)
 
 
 /* Last in this context does not mean newest */
-uint32_t SMBCALL smb_last_in_thread(smb_t* smb, smbmsg_t* remsg)
+uint32_t smb_last_in_thread(smb_t* smb, smbmsg_t* remsg)
 {
 	smbmsg_t msg;
 	memset(&msg, 0, sizeof(msg));
@@ -2140,6 +2212,8 @@ uint32_t SMBCALL smb_last_in_thread(smb_t* smb, smbmsg_t* remsg)
 
 SMBEXPORT enum smb_msg_type smb_msg_type(smb_msg_attr_t attr)
 {
+	if(attr & MSG_FILE)
+		return SMB_MSG_TYPE_FILE;
 	switch (attr&MSG_POLL_VOTE_MASK) {
 		case 0:
 			return SMB_MSG_TYPE_NORMAL;
@@ -2154,13 +2228,13 @@ SMBEXPORT enum smb_msg_type smb_msg_type(smb_msg_attr_t attr)
 
 // Return count of messages of the desired types (bit-mask), as read from index
 // Does so as fast as possible, without locking
-SMBEXPORT size_t SMBCALL smb_msg_count(smb_t* smb, unsigned types)
+SMBEXPORT size_t smb_msg_count(smb_t* smb, unsigned types)
 {
 	off_t index_length = filelength(fileno(smb->sid_fp));
 	if(index_length < sizeof(idxrec_t))
 		return 0;
 
-	uint32_t total = index_length / sizeof(idxrec_t);
+	uint32_t total = (uint32_t)(index_length / sizeof(idxrec_t));
 	if(total < 1)
 		return 0;
 
diff --git a/src/smblib/smblib.h b/src/smblib/smblib.h
index fbe37ffbf99c2fc1101c72c210673328e7ba8063..660db76ff3cf01b313ed898af573cc99d2b85ca6 100644
--- a/src/smblib/smblib.h
+++ b/src/smblib/smblib.h
@@ -1,8 +1,5 @@
 /* Synchronet message base (SMB) library function prototypes */
 
-/* $Id: smblib.h,v 1.99 2020/05/25 00:39:47 rswindell Exp $ */
-// vi: tabstop=4
-
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
  * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
@@ -16,21 +13,9 @@
  * See the GNU Lesser General Public License for more details: lgpl.txt or	*
  * http://www.fsf.org/copyleft/lesser.html									*
  *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
- *																			*
  * For Synchronet coding style and modification guidelines, see				*
  * http://www.synchro.net/source.html										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
@@ -42,11 +27,6 @@
 #endif
 
 #ifdef _WIN32
-	#ifdef __BORLANDC__
-		#define SMBCALL
-	#else
-		#define SMBCALL
-	#endif
 	#if defined(SMB_IMPORTS) || defined(SMB_EXPORTS)
 		#if defined(SMB_IMPORTS)
 			#define SMBEXPORT __declspec( dllimport )
@@ -56,11 +36,7 @@
 	#else	/* self-contained executable */
 		#define SMBEXPORT
 	#endif
-#elif defined __unix__
-	#define SMBCALL
-	#define SMBEXPORT
 #else
-	#define SMBCALL
 	#define SMBEXPORT
 #endif
 
@@ -70,6 +46,7 @@
 #define SMB_SUCCESS			0			/* Successful result/return code */
 #define SMB_FAILURE			-1			/* Generic error (discouraged) */
 #define SMB_BAD_PARAMETER	-2			/* Invalid API function parameter value */
+
 										/* Standard smblib errors values */
 #define SMB_ERR_NOT_OPEN	-100		/* Message base not open */
 #define SMB_ERR_HDR_LEN		-101		/* Invalid message header length (>64k) */
@@ -90,6 +67,7 @@
 #define SMB_ERR_FILE_LEN	-206		/* File length invalid */
 #define SMB_ERR_DELETE		-207		/* File deletion error */
 #define SMB_ERR_UNLOCK		-208		/* File unlock error */
+#define SMB_ERR_RENAME		-209		/* File rename error */
 #define SMB_ERR_MEM			-300		/* Memory allocation error */
 
 #define SMB_DUPE_MSG		1			/* Duplicate message detected by smb_addcrc() */
@@ -125,42 +103,42 @@
 extern "C" {
 #endif
 
-SMBEXPORT int 		SMBCALL smb_ver(void);
-SMBEXPORT char*		SMBCALL smb_lib_ver(void);
-SMBEXPORT int 		SMBCALL smb_open(smb_t* smb);
-SMBEXPORT int		SMBCALL smb_open_index(smb_t* smb);
-SMBEXPORT void		SMBCALL smb_close(smb_t* smb);
-SMBEXPORT int 		SMBCALL smb_initsmbhdr(smb_t* smb);
-SMBEXPORT int 		SMBCALL smb_create(smb_t* smb);
-SMBEXPORT int 		SMBCALL smb_trunchdr(smb_t* smb);
-SMBEXPORT int		SMBCALL smb_lock(smb_t* smb);
-SMBEXPORT int		SMBCALL smb_unlock(smb_t* smb);
-SMBEXPORT BOOL		SMBCALL smb_islocked(smb_t* smb);
-SMBEXPORT int		SMBCALL Smb_initsmbhdr(smb_t* smb);
-SMBEXPORT int 		SMBCALL smb_locksmbhdr(smb_t* smb);
-SMBEXPORT int 		SMBCALL smb_getstatus(smb_t* smb);
-SMBEXPORT int 		SMBCALL smb_putstatus(smb_t* smb);
-SMBEXPORT int 		SMBCALL smb_unlocksmbhdr(smb_t* smb);
-SMBEXPORT int 		SMBCALL smb_getmsgidx(smb_t* smb, smbmsg_t* msg);
-SMBEXPORT int 		SMBCALL smb_getfirstidx(smb_t* smb, idxrec_t *idx);
-SMBEXPORT int 		SMBCALL smb_getlastidx(smb_t* smb, idxrec_t *idx);
-SMBEXPORT ulong		SMBCALL smb_getmsghdrlen(smbmsg_t* msg);
-SMBEXPORT ulong		SMBCALL smb_getmsgdatlen(smbmsg_t* msg);
-SMBEXPORT ulong		SMBCALL smb_getmsgtxtlen(smbmsg_t* msg);
-SMBEXPORT int 		SMBCALL smb_lockmsghdr(smb_t* smb, smbmsg_t* msg);
-SMBEXPORT int 		SMBCALL smb_getmsghdr(smb_t* smb, smbmsg_t* msg);
-SMBEXPORT int 		SMBCALL smb_unlockmsghdr(smb_t* smb, smbmsg_t* msg);
-SMBEXPORT int 		SMBCALL smb_addcrc(smb_t* smb, uint32_t crc);
+SMBEXPORT int 		smb_ver(void);
+SMBEXPORT char*		smb_lib_ver(void);
+SMBEXPORT int 		smb_open(smb_t*);
+SMBEXPORT int		smb_open_index(smb_t*);
+SMBEXPORT void		smb_close(smb_t*);
+SMBEXPORT int 		smb_initsmbhdr(smb_t*);
+SMBEXPORT int 		smb_create(smb_t*);
+SMBEXPORT int 		smb_trunchdr(smb_t*);
+SMBEXPORT int		smb_lock(smb_t*);
+SMBEXPORT int		smb_unlock(smb_t*);
+SMBEXPORT BOOL		smb_islocked(smb_t*);
+SMBEXPORT int		Smb_initsmbhdr(smb_t*);
+SMBEXPORT int 		smb_locksmbhdr(smb_t*);
+SMBEXPORT int 		smb_getstatus(smb_t*);
+SMBEXPORT int 		smb_putstatus(smb_t*);
+SMBEXPORT int 		smb_unlocksmbhdr(smb_t*);
+SMBEXPORT int 		smb_getmsgidx(smb_t*, smbmsg_t*);
+SMBEXPORT int 		smb_getfirstidx(smb_t*, idxrec_t*);
+SMBEXPORT int 		smb_getlastidx(smb_t*, idxrec_t*);
+SMBEXPORT ulong		smb_getmsghdrlen(smbmsg_t*);
+SMBEXPORT ulong		smb_getmsgdatlen(smbmsg_t*);
+SMBEXPORT ulong		smb_getmsgtxtlen(smbmsg_t*);
+SMBEXPORT int 		smb_lockmsghdr(smb_t*, smbmsg_t*);
+SMBEXPORT int 		smb_getmsghdr(smb_t*, smbmsg_t*);
+SMBEXPORT int 		smb_unlockmsghdr(smb_t*, smbmsg_t*);
+SMBEXPORT int 		smb_addcrc(smb_t*, uint32_t crc);
 
-SMBEXPORT int 		SMBCALL smb_hfield_add(smbmsg_t* msg, uint16_t type, size_t length, void* data, BOOL insert);
-SMBEXPORT int		SMBCALL smb_hfield_add_str(smbmsg_t* msg, uint16_t type, const char* str, BOOL insert);
-SMBEXPORT int		SMBCALL	smb_hfield_replace(smbmsg_t* msg, uint16_t type, size_t length, void* data);
-SMBEXPORT int		SMBCALL	smb_hfield_replace_str(smbmsg_t* msg, uint16_t type, const char* str);
-SMBEXPORT int		SMBCALL smb_hfield_append(smbmsg_t* msg, uint16_t type, size_t length, void* data);
-SMBEXPORT int		SMBCALL smb_hfield_append_str(smbmsg_t* msg, uint16_t type, const char* data);
-SMBEXPORT int		SMBCALL smb_hfield_add_list(smbmsg_t* msg, hfield_t** hfield_list, void** hfield_dat, BOOL insert);
-SMBEXPORT int		SMBCALL smb_hfield_add_netaddr(smbmsg_t* msg, uint16_t type, const char* str, uint16_t* nettype, BOOL insert);
-SMBEXPORT int		SMBCALL	smb_hfield_string(smbmsg_t*, uint16_t type, const char*);
+SMBEXPORT int 		smb_hfield_add(smbmsg_t*, uint16_t type, size_t, void* data, BOOL insert);
+SMBEXPORT int		smb_hfield_add_str(smbmsg_t*, uint16_t type, const char* str, BOOL insert);
+SMBEXPORT int		smb_hfield_replace(smbmsg_t*, uint16_t type, size_t, void* data);
+SMBEXPORT int		smb_hfield_replace_str(smbmsg_t*, uint16_t type, const char* str);
+SMBEXPORT int		smb_hfield_append(smbmsg_t*, uint16_t type, size_t, void* data);
+SMBEXPORT int		smb_hfield_append_str(smbmsg_t*, uint16_t type, const char* data);
+SMBEXPORT int		smb_hfield_add_list(smbmsg_t*, hfield_t** hfield_list, void** hfield_dat, BOOL insert);
+SMBEXPORT int		smb_hfield_add_netaddr(smbmsg_t*, uint16_t type, const char* str, uint16_t* nettype, BOOL insert);
+SMBEXPORT int		smb_hfield_string(smbmsg_t*, uint16_t type, const char*);
 /* Convenience macro: */
 #define smb_hfield_bin(msg, type, data) smb_hfield_add(msg, type, sizeof(data), &(data), /* insert: */FALSE)
 /* Backward compatibility macros: */
@@ -168,75 +146,80 @@ SMBEXPORT int		SMBCALL	smb_hfield_string(smbmsg_t*, uint16_t type, const char*);
 #define smb_hfield_str(msg, type, str)	smb_hfield_add_str(msg, type, str, /* insert: */FALSE)
 #define smb_hfield_netaddr(msg, type, str, nettype) smb_hfield_add_netaddr(msg, type, str, nettype, /* insert: */FALSE)
 
-SMBEXPORT int 		SMBCALL smb_dfield(smbmsg_t* msg, uint16_t type, ulong length);
-SMBEXPORT void*		SMBCALL smb_get_hfield(smbmsg_t* msg, uint16_t type, hfield_t** hfield);
-SMBEXPORT int 		SMBCALL smb_addmsghdr(smb_t* smb, smbmsg_t* msg, int storage);
-SMBEXPORT int 		SMBCALL smb_putmsg(smb_t* smb, smbmsg_t* msg);
-SMBEXPORT int 		SMBCALL smb_putmsgidx(smb_t* smb, smbmsg_t* msg);
-SMBEXPORT int 		SMBCALL smb_putmsghdr(smb_t* smb, smbmsg_t* msg);
-SMBEXPORT void		SMBCALL smb_freemsgmem(smbmsg_t* msg);
-SMBEXPORT void		SMBCALL smb_freemsghdrmem(smbmsg_t* msg);
-SMBEXPORT ulong		SMBCALL smb_hdrblocks(ulong length);
-SMBEXPORT ulong		SMBCALL smb_datblocks(ulong length);
-SMBEXPORT int		SMBCALL	smb_copymsgmem(smb_t* smb, smbmsg_t* destmsg, smbmsg_t* srcmsg);
-SMBEXPORT int		SMBCALL smb_tzutc(int16_t timezone);
-SMBEXPORT int		SMBCALL smb_updatethread(smb_t* smb, smbmsg_t* remsg, ulong newmsgnum);
-SMBEXPORT int		SMBCALL smb_updatemsg(smb_t* smb, smbmsg_t* msg);
-SMBEXPORT BOOL		SMBCALL smb_valid_hdr_offset(smb_t* smb, ulong offset);
-SMBEXPORT int		SMBCALL smb_init_idx(smb_t* smb, smbmsg_t* msg);
-SMBEXPORT uint16_t	SMBCALL	smb_voted_already(smb_t*, uint32_t msgnum, const char* name, enum smb_net_type, void* net_addr);
-SMBEXPORT BOOL		SMBCALL smb_msg_is_from(smbmsg_t* msg, const char* name, enum smb_net_type net_type, const void* net_addr);
-SMBEXPORT uint32_t	SMBCALL smb_first_in_thread(smb_t*, smbmsg_t*, msghdr_t*);
-SMBEXPORT uint32_t	SMBCALL smb_next_in_thread(smb_t*, smbmsg_t*, msghdr_t*);
-SMBEXPORT uint32_t	SMBCALL smb_last_in_branch(smb_t*, smbmsg_t*);
-SMBEXPORT uint32_t	SMBCALL smb_last_in_thread(smb_t*, smbmsg_t*);
-SMBEXPORT BOOL		SMBCALL smb_msg_is_utf8(const smbmsg_t*);
-SMBEXPORT size_t	SMBCALL smb_msg_count(smb_t*, unsigned types);
+SMBEXPORT int 		smb_dfield(smbmsg_t*, uint16_t type, ulong length);
+SMBEXPORT void*		smb_get_hfield(smbmsg_t*, uint16_t type, hfield_t** hfield);
+SMBEXPORT int		smb_new_hfield(smbmsg_t*, uint16_t type, size_t, void* data);
+SMBEXPORT int		smb_new_hfield_str(smbmsg_t*, uint16_t type, const char*);
+SMBEXPORT int 		smb_addmsghdr(smb_t*, smbmsg_t*, int storage);
+SMBEXPORT int 		smb_putmsg(smb_t*, smbmsg_t*);
+SMBEXPORT int 		smb_putmsgidx(smb_t*, smbmsg_t*);
+SMBEXPORT int 		smb_putmsghdr(smb_t*, smbmsg_t*);
+SMBEXPORT void		smb_freemsgmem(smbmsg_t*);
+SMBEXPORT void		smb_freemsghdrmem(smbmsg_t*);
+SMBEXPORT ulong		smb_hdrblocks(ulong length);
+SMBEXPORT ulong		smb_datblocks(off_t length);
+SMBEXPORT int		smb_copymsgmem(smb_t*, smbmsg_t* destmsg, smbmsg_t* srcmsg);
+SMBEXPORT int		smb_tzutc(int16_t timezone);
+SMBEXPORT int		smb_updatethread(smb_t*, smbmsg_t* remsg, ulong newmsgnum);
+SMBEXPORT int		smb_updatemsg(smb_t*, smbmsg_t*);
+SMBEXPORT BOOL		smb_valid_hdr_offset(smb_t*, ulong offset);
+SMBEXPORT int		smb_init_idx(smb_t*, smbmsg_t*);
+SMBEXPORT uint16_t	smb_voted_already(smb_t*, uint32_t msgnum, const char* name, enum smb_net_type, void* net_addr);
+SMBEXPORT BOOL		smb_msg_is_from(smbmsg_t*, const char* name, enum smb_net_type net_type, const void* net_addr);
+SMBEXPORT uint32_t	smb_first_in_thread(smb_t*, smbmsg_t*, msghdr_t*);
+SMBEXPORT uint32_t	smb_next_in_thread(smb_t*, smbmsg_t*, msghdr_t*);
+SMBEXPORT uint32_t	smb_last_in_branch(smb_t*, smbmsg_t*);
+SMBEXPORT uint32_t	smb_last_in_thread(smb_t*, smbmsg_t*);
+SMBEXPORT size_t	smb_idxreclen(smb_t*);
+SMBEXPORT uint32_t	smb_count_idx_records(smb_t*, uint16_t mask, uint16_t cmp);
+SMBEXPORT BOOL		smb_msg_is_utf8(const smbmsg_t*);
+SMBEXPORT size_t	smb_msg_count(smb_t*, unsigned types);
 SMBEXPORT enum smb_msg_type smb_msg_type(smb_msg_attr_t);
 
 /* smbadd.c */
-SMBEXPORT int		SMBCALL smb_addmsg(smb_t* smb, smbmsg_t* msg, int storage, long dupechk_hashes
+SMBEXPORT int		smb_addmsg(smb_t*, smbmsg_t*, int storage, long dupechk_hashes
 						,uint16_t xlat, const uchar* body, const uchar* tail);
-SMBEXPORT int		SMBCALL smb_addvote(smb_t* smb, smbmsg_t* msg, int storage);
-SMBEXPORT int		SMBCALL smb_addpoll(smb_t* smb, smbmsg_t* msg, int storage);
-SMBEXPORT int		SMBCALL smb_addpollclosure(smb_t* smb, smbmsg_t* msg, int storage);
+SMBEXPORT int		smb_addvote(smb_t*, smbmsg_t*, int storage);
+SMBEXPORT int		smb_addpoll(smb_t*, smbmsg_t*, int storage);
+SMBEXPORT int		smb_addpollclosure(smb_t*, smbmsg_t*, int storage);
 
 /* smballoc.c */
-SMBEXPORT long		SMBCALL smb_allochdr(smb_t* smb, ulong length);
-SMBEXPORT long		SMBCALL smb_fallochdr(smb_t* smb, ulong length);
-SMBEXPORT long		SMBCALL smb_hallochdr(smb_t* smb);
-SMBEXPORT long		SMBCALL smb_allocdat(smb_t* smb, ulong length, uint16_t int16_trefs);
-SMBEXPORT long		SMBCALL smb_fallocdat(smb_t* smb, ulong length, uint16_t refs);
-SMBEXPORT long		SMBCALL smb_hallocdat(smb_t* smb);
-SMBEXPORT int		SMBCALL smb_incmsg_dfields(smb_t* smb, smbmsg_t* msg, uint16_t refs);
-SMBEXPORT int 		SMBCALL smb_incmsgdat(smb_t* smb, ulong offset, ulong length, uint16_t refs);
-SMBEXPORT int 		SMBCALL smb_freemsg(smb_t* smb, smbmsg_t* msg);
-SMBEXPORT int		SMBCALL smb_freemsg_dfields(smb_t* smb, smbmsg_t* msg, uint16_t refs);
-SMBEXPORT int 		SMBCALL smb_freemsgdat(smb_t* smb, ulong offset, ulong length, uint16_t refs);
-SMBEXPORT int 		SMBCALL smb_freemsghdr(smb_t* smb, ulong offset, ulong length);
-SMBEXPORT void		SMBCALL smb_freemsgtxt(char* buf);
+SMBEXPORT off_t		smb_allochdr(smb_t*, ulong length);
+SMBEXPORT off_t		smb_fallochdr(smb_t*, ulong length);
+SMBEXPORT off_t		smb_hallochdr(smb_t*);
+SMBEXPORT off_t		smb_allocdat(smb_t*, off_t length, uint16_t int16_trefs);
+SMBEXPORT off_t		smb_fallocdat(smb_t*, off_t length, uint16_t refs);
+SMBEXPORT off_t		smb_hallocdat(smb_t*);
+SMBEXPORT int		smb_incmsg_dfields(smb_t*, smbmsg_t*, uint16_t refs);
+SMBEXPORT int 		smb_incmsgdat(smb_t*, off_t offset, ulong length, uint16_t refs);
+SMBEXPORT int 		smb_freemsg(smb_t*, smbmsg_t*);
+SMBEXPORT int		smb_freemsg_dfields(smb_t*, smbmsg_t*, uint16_t refs);
+SMBEXPORT int 		smb_freemsgdat(smb_t*, off_t offset, ulong length, uint16_t refs);
+SMBEXPORT int 		smb_freemsghdr(smb_t*, off_t offset, ulong length);
+SMBEXPORT void		smb_freemsgtxt(char* buf);
 
 /* smbhash.c */
-SMBEXPORT int		SMBCALL smb_findhash(smb_t* smb, hash_t** compare_list, hash_t* found
+SMBEXPORT int		smb_findhash(smb_t*, hash_t** compare_list, hash_t* found
 										 ,long source_mask, BOOL mark);
-SMBEXPORT int		SMBCALL smb_hashmsg(smb_t* smb, smbmsg_t* msg, const uchar* text, BOOL update);
-SMBEXPORT hash_t*	SMBCALL	smb_hash(ulong msgnum, uint32_t time, unsigned source
-								,unsigned flags, const void* data, size_t length);
-SMBEXPORT hash_t*	SMBCALL	smb_hashstr(ulong msgnum, uint32_t time, unsigned source
+SMBEXPORT int		smb_hashmsg(smb_t*, smbmsg_t*, const uchar* text, BOOL update);
+SMBEXPORT hash_t*	smb_hash(ulong msgnum, uint32_t time, unsigned source
+								,unsigned flags, const void* data, size_t);
+SMBEXPORT hash_t*	smb_hashstr(ulong msgnum, uint32_t time, unsigned source
 								,unsigned flags, const char* str);
 
-SMBEXPORT hash_t**	SMBCALL smb_msghashes(smbmsg_t* msg, const uchar* text, long source_mask);
-SMBEXPORT int		SMBCALL smb_addhashes(smb_t* smb, hash_t** hash_list, BOOL skip_marked);
-SMBEXPORT uint16_t	SMBCALL smb_name_crc(const char* name);
-SMBEXPORT uint16_t	SMBCALL smb_subject_crc(const char *subj);
-SMBEXPORT void		SMBCALL smb_freehashes(hash_t**);
-SMBEXPORT long		SMBCALL	smb_getmsgidx_by_time(smb_t*, idxrec_t*, time_t);
+SMBEXPORT hash_t**	smb_msghashes(smbmsg_t*, const uchar* text, long source_mask);
+SMBEXPORT int		smb_addhashes(smb_t*, hash_t** hash_list, BOOL skip_marked);
+SMBEXPORT uint16_t	smb_name_crc(const char* name);
+SMBEXPORT uint16_t	smb_subject_crc(const char *subj);
+SMBEXPORT void		smb_freehashes(hash_t**);
+SMBEXPORT long		smb_getmsgidx_by_time(smb_t*, idxrec_t*, time_t);
+SMBEXPORT int		smb_hashfile(const char* path, off_t, struct hash_data*);
 
 /* Fast look-up functions (using hashes) */
-SMBEXPORT int 		SMBCALL smb_getmsgidx_by_hash(smb_t* smb, smbmsg_t* msg, unsigned source
-								 ,unsigned flags, const void* data, size_t length);
-SMBEXPORT int 		SMBCALL smb_getmsghdr_by_hash(smb_t* smb, smbmsg_t* msg, unsigned source
-								 ,unsigned flags, const void* data, size_t length);
+SMBEXPORT int 		smb_getmsgidx_by_hash(smb_t*, smbmsg_t*, unsigned source
+								 ,unsigned flags, const void* data, size_t);
+SMBEXPORT int 		smb_getmsghdr_by_hash(smb_t*, smbmsg_t*, unsigned source
+								 ,unsigned flags, const void* data, size_t);
 
 /* 0-length specifies ASCIIZ data (length calculated automatically) */
 #define smb_getmsgidx_by_hashstr(smb, msg, source, flags, data) \
@@ -255,49 +238,61 @@ SMBEXPORT int 		SMBCALL smb_getmsghdr_by_hash(smb_t* smb, smbmsg_t* msg, unsigne
 		smb_getmsghdr_by_hashstr(smb, msg, SMB_HASH_SOURCE_FTN_ID, SMB_HASH_MASK, id)
 
 /* smbstr.c */
-SMBEXPORT char*		SMBCALL smb_hfieldtype(uint16_t type);
-SMBEXPORT uint16_t	SMBCALL smb_hfieldtypelookup(const char*);
-SMBEXPORT char*		SMBCALL smb_dfieldtype(uint16_t type);
-SMBEXPORT char*		SMBCALL smb_faddrtoa(fidoaddr_t* addr, char* outstr);
-SMBEXPORT char*		SMBCALL smb_netaddr(net_t* net);
-SMBEXPORT char*		SMBCALL smb_netaddrstr(net_t* net, char* fidoaddr_buf);
-SMBEXPORT char*		SMBCALL	smb_nettype(enum smb_net_type);
-SMBEXPORT char*		SMBCALL smb_zonestr(int16_t zone, char* outstr);
-SMBEXPORT char*		SMBCALL smb_msgattrstr(int16_t attr, char* outstr, size_t maxlen);
-SMBEXPORT char*		SMBCALL smb_auxattrstr(int32_t attr, char* outstr, size_t maxlen);
-SMBEXPORT char*		SMBCALL smb_netattrstr(int32_t attr, char* outstr, size_t maxlen);
-SMBEXPORT char*		SMBCALL smb_hashsource(smbmsg_t* msg, int source);
-SMBEXPORT char*		SMBCALL smb_hashsourcetype(uchar type);
-SMBEXPORT fidoaddr_t SMBCALL smb_atofaddr(const fidoaddr_t* sys_addr, const char *str);
-SMBEXPORT enum smb_net_type SMBCALL smb_netaddr_type(const char* addr);
-SMBEXPORT enum smb_net_type SMBCALL smb_get_net_type_by_addr(const char* addr);
+SMBEXPORT char*		smb_hfieldtype(uint16_t type);
+SMBEXPORT uint16_t	smb_hfieldtypelookup(const char*);
+SMBEXPORT char*		smb_dfieldtype(uint16_t type);
+SMBEXPORT char*		smb_faddrtoa(fidoaddr_t* addr, char* outstr);
+SMBEXPORT char*		smb_netaddr(net_t* net);
+SMBEXPORT char*		smb_netaddrstr(net_t* net, char* fidoaddr_buf);
+SMBEXPORT char*		smb_nettype(enum smb_net_type);
+SMBEXPORT char*		smb_zonestr(int16_t zone, char* outstr);
+SMBEXPORT char*		smb_msgattrstr(int16_t attr, char* outstr, size_t maxlen);
+SMBEXPORT char*		smb_auxattrstr(int32_t attr, char* outstr, size_t maxlen);
+SMBEXPORT char*		smb_netattrstr(int32_t attr, char* outstr, size_t maxlen);
+SMBEXPORT char*		smb_hashsource(smbmsg_t*, int source);
+SMBEXPORT char*		smb_hashsourcetype(uchar type);
+SMBEXPORT fidoaddr_t smb_atofaddr(const fidoaddr_t* sys_addr, const char *str);
+SMBEXPORT enum smb_net_type smb_netaddr_type(const char* addr);
+SMBEXPORT enum smb_net_type smb_get_net_type_by_addr(const char* addr);
 /* smbdump.c */
-SMBEXPORT void		SMBCALL smb_dump_msghdr(FILE*, smbmsg_t*);
-SMBEXPORT str_list_t SMBCALL smb_msghdr_str_list(smbmsg_t*);
+SMBEXPORT void		smb_dump_msghdr(FILE*, smbmsg_t*);
+SMBEXPORT str_list_t smb_msghdr_str_list(smbmsg_t*);
 
 /* smbtxt.c */
-SMBEXPORT char*		SMBCALL smb_getmsgtxt(smb_t*, smbmsg_t*, ulong mode);
-SMBEXPORT char*		SMBCALL smb_getplaintext(smbmsg_t*, char* body);
-SMBEXPORT uint8_t*	SMBCALL smb_getattachment(smbmsg_t*, char* body, char* filename, size_t filename_len, uint32_t* filelen, int index);
-SMBEXPORT ulong		SMBCALL	smb_countattachments(smb_t*, smbmsg_t*, const char* body);
-SMBEXPORT void		SMBCALL smb_parse_content_type(const char* content_type, char** subtype, char** charset);
+SMBEXPORT char*		smb_getmsgtxt(smb_t*, smbmsg_t*, ulong mode);
+SMBEXPORT char*		smb_getplaintext(smbmsg_t*, char* body);
+SMBEXPORT uint8_t*	smb_getattachment(smbmsg_t*, char* body, char* filename, size_t filename_len, uint32_t* filelen, int index);
+SMBEXPORT ulong		smb_countattachments(smb_t*, smbmsg_t*, const char* body);
+SMBEXPORT void		smb_parse_content_type(const char* content_type, char** subtype, char** charset);
 
 /* smbfile.c */
-SMBEXPORT int 		SMBCALL smb_feof(FILE* fp);
-SMBEXPORT int 		SMBCALL smb_ferror(FILE* fp);
-SMBEXPORT int 		SMBCALL smb_fflush(FILE* fp);
-SMBEXPORT int 		SMBCALL smb_fgetc(FILE* fp);
-SMBEXPORT int 		SMBCALL smb_fputc(int ch, FILE* fp);
-SMBEXPORT int 		SMBCALL smb_fseek(FILE* fp, long offset, int whence);
-SMBEXPORT long		SMBCALL smb_ftell(FILE* fp);
-SMBEXPORT size_t	SMBCALL smb_fread(smb_t*, void* buf, size_t bytes, FILE* fp);
-SMBEXPORT size_t	SMBCALL smb_fwrite(smb_t*, const void* buf, size_t bytes, FILE* fp);
-SMBEXPORT long		SMBCALL smb_fgetlength(FILE* fp);
-SMBEXPORT int 		SMBCALL smb_fsetlength(FILE* fp, long length);
-SMBEXPORT void		SMBCALL smb_rewind(FILE* fp);
-SMBEXPORT void		SMBCALL smb_clearerr(FILE* fp);
-SMBEXPORT int 		SMBCALL smb_open_fp(smb_t* smb, FILE**, int share);
-SMBEXPORT void		SMBCALL smb_close_fp(FILE**);
+SMBEXPORT int 		smb_feof(FILE* fp);
+SMBEXPORT int 		smb_ferror(FILE* fp);
+SMBEXPORT int 		smb_fflush(FILE* fp);
+SMBEXPORT int 		smb_fgetc(FILE* fp);
+SMBEXPORT int 		smb_fputc(int ch, FILE* fp);
+SMBEXPORT int 		smb_fseek(FILE* fp, off_t offset, int whence);
+SMBEXPORT off_t		smb_ftell(FILE* fp);
+SMBEXPORT size_t	smb_fread(smb_t*, void* buf, size_t bytes, FILE* fp);
+SMBEXPORT size_t	smb_fwrite(smb_t*, const void* buf, size_t bytes, FILE* fp);
+SMBEXPORT off_t		smb_fgetlength(FILE* fp);
+SMBEXPORT int 		smb_fsetlength(FILE* fp, long length);
+SMBEXPORT void		smb_rewind(FILE* fp);
+SMBEXPORT void		smb_clearerr(FILE* fp);
+SMBEXPORT int 		smb_open_fp(smb_t*, FILE**, int share);
+SMBEXPORT void		smb_close_fp(FILE**);
+
+/* New FileBase API: */
+enum file_detail { file_detail_index, file_detail_normal, file_detail_extdesc };
+SMBEXPORT int		smb_addfile(smb_t*, smbfile_t*, int storage, const char* extdesc, const char* path);
+SMBEXPORT int		smb_renewfile(smb_t*, smbfile_t*, int storage, const char* path);
+SMBEXPORT int		smb_getfile(smb_t*, smbfile_t*, enum file_detail);
+SMBEXPORT int		smb_putfile(smb_t*, smbfile_t*);
+SMBEXPORT int		smb_findfile(smb_t*, const char* filename, smbfile_t*);
+SMBEXPORT int		smb_loadfile(smb_t*, const char* filename, smbfile_t*, enum file_detail);
+SMBEXPORT void		smb_freefilemem(smbfile_t*);
+SMBEXPORT int		smb_removefile(smb_t*, smbfile_t*);
+SMBEXPORT char*		smb_fileidxname(const char* filename, char* buf, size_t);
 
 #ifdef __cplusplus
 }
diff --git a/src/smblib/smblib.vcxproj b/src/smblib/smblib.vcxproj
index bec0680c3d9969f4d481e6ada0fad7c5b6ef2896..c0ee164868be304272a94474a77a50facea90846 100644
--- a/src/smblib/smblib.vcxproj
+++ b/src/smblib/smblib.vcxproj
@@ -118,6 +118,7 @@
     <ClCompile Include="..\hash\crc16.c" />
     <ClCompile Include="..\hash\crc32.c" />
     <ClCompile Include="..\hash\md5.c" />
+    <ClCompile Include="..\hash\sha1.c" />
     <ClCompile Include="smbadd.c">
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
diff --git a/src/smblib/smbstr.c b/src/smblib/smbstr.c
index 245f4ef4020e3039830a4d52dd6c352988362819..365b9b3b154604919070399cb3ccc3303b8d3b2b 100644
--- a/src/smblib/smbstr.c
+++ b/src/smblib/smbstr.c
@@ -23,7 +23,7 @@
 #include <genwrap.h> 		/* stricmp */
 #include "smblib.h"
 
-char* SMBCALL smb_hfieldtype(uint16_t type)
+char* smb_hfieldtype(uint16_t type)
 {
 	static char str[8];
 
@@ -113,7 +113,7 @@ char* SMBCALL smb_hfieldtype(uint16_t type)
 	return(str);
 }
 
-uint16_t SMBCALL smb_hfieldtypelookup(const char* str)
+uint16_t smb_hfieldtypelookup(const char* str)
 {
 	uint16_t type;
 
@@ -127,7 +127,7 @@ uint16_t SMBCALL smb_hfieldtypelookup(const char* str)
 	return(UNKNOWN);
 }
 
-char* SMBCALL smb_dfieldtype(uint16_t type)
+char* smb_dfieldtype(uint16_t type)
 {
 	static char str[8];
 
@@ -140,7 +140,7 @@ char* SMBCALL smb_dfieldtype(uint16_t type)
 	return(str);
 }
 
-char* SMBCALL smb_hashsourcetype(uchar type)
+char* smb_hashsourcetype(uchar type)
 {
 	static char str[8];
 
@@ -154,7 +154,7 @@ char* SMBCALL smb_hashsourcetype(uchar type)
 	return(str);
 }
 
-char* SMBCALL smb_hashsource(smbmsg_t* msg, int source)
+char* smb_hashsource(smbmsg_t* msg, int source)
 {
 	switch(source) {
 		case SMB_HASH_SOURCE_MSG_ID:
@@ -170,7 +170,7 @@ char* SMBCALL smb_hashsource(smbmsg_t* msg, int source)
 /****************************************************************************/
 /* Converts when_t.zone into ASCII format                                   */
 /****************************************************************************/
-char* SMBCALL smb_zonestr(int16_t zone, char* str)
+char* smb_zonestr(int16_t zone, char* str)
 {
 	char*		plus;
     static char buf[32];
@@ -248,7 +248,7 @@ char* SMBCALL smb_zonestr(int16_t zone, char* str)
 /****************************************************************************/
 /* Returns an ASCII string for FidoNet address 'addr'                       */
 /****************************************************************************/
-char* SMBCALL smb_faddrtoa(fidoaddr_t* addr, char* str)
+char* smb_faddrtoa(fidoaddr_t* addr, char* str)
 {
 	static char buf[64];
     char point[25];
@@ -268,7 +268,7 @@ char* SMBCALL smb_faddrtoa(fidoaddr_t* addr, char* str)
 /****************************************************************************/
 /* Returns the FidoNet address parsed from str.								*/
 /****************************************************************************/
-fidoaddr_t SMBCALL smb_atofaddr(const fidoaddr_t* sys_addr, const char *str)
+fidoaddr_t smb_atofaddr(const fidoaddr_t* sys_addr, const char *str)
 {
 	char*		p;
 	const char*	terminator;
@@ -306,7 +306,7 @@ fidoaddr_t SMBCALL smb_atofaddr(const fidoaddr_t* sys_addr, const char *str)
 /* Returns ASCIIZ representation of network address (net_t)					*/
 /* NOT THREAD-SAFE!															*/
 /****************************************************************************/
-char* SMBCALL smb_netaddr(net_t* net)
+char* smb_netaddr(net_t* net)
 {
 	return(smb_netaddrstr(net, NULL));
 }
@@ -314,7 +314,7 @@ char* SMBCALL smb_netaddr(net_t* net)
 /****************************************************************************/
 /* Copies ASCIIZ representation of network address (net_t) into buf			*/
 /****************************************************************************/
-char* SMBCALL smb_netaddrstr(net_t* net, char* fidoaddr_buf)
+char* smb_netaddrstr(net_t* net, char* fidoaddr_buf)
 {
 	if(net->type==NET_FIDO)
 		return(smb_faddrtoa((fidoaddr_t*)net->addr,fidoaddr_buf));
@@ -326,7 +326,7 @@ char* SMBCALL smb_netaddrstr(net_t* net, char* fidoaddr_buf)
 /* QWKnet and Internet addresses must have an '@'.							*/
 /* FidoNet addresses may be in form: "user@addr" or just "addr".			*/
 /****************************************************************************/
-enum smb_net_type SMBCALL smb_netaddr_type(const char* str)
+enum smb_net_type smb_netaddr_type(const char* str)
 {
 	const char*	p;
 
@@ -365,7 +365,7 @@ enum smb_net_type SMBCALL smb_netaddr_type(const char* str)
 /*	"someone@anywhere"	= NET_INTERNET										*/
 /*	"someone@some.host"	= NET_INTERNET										*/
 /****************************************************************************/
-enum smb_net_type SMBCALL smb_get_net_type_by_addr(const char* addr)
+enum smb_net_type smb_get_net_type_by_addr(const char* addr)
 {
 	const char*	p = addr;
 	const char*	tp;
@@ -420,7 +420,7 @@ enum smb_net_type SMBCALL smb_get_net_type_by_addr(const char* addr)
 	return NET_UNKNOWN;
 }
 
-char* SMBCALL smb_nettype(enum smb_net_type type)
+char* smb_nettype(enum smb_net_type type)
 {
 	switch(type) {
 		case NET_NONE:		return "NONE";
diff --git a/src/xpdev/dirwrap.c b/src/xpdev/dirwrap.c
index 44122d0e70b927920940b4cc686ff11f34a3e0bd..5f996cb76ee7148e45bb04a2a4d6119a0d0b9a5d 100644
--- a/src/xpdev/dirwrap.c
+++ b/src/xpdev/dirwrap.c
@@ -60,7 +60,6 @@
 #endif
 
 #include <sys/types.h>	/* _dev_t */
-#include <sys/stat.h>	/* struct stat */
 
 #include <stdio.h>		/* sprintf */
 #include <stdlib.h>		/* rand */
@@ -68,7 +67,7 @@
 
 #include "genwrap.h"	/* strupr/strlwr */
 #include "dirwrap.h"	/* DLLCALL */
-#include "filewrap.h"	/* filetime() */
+#include "filewrap.h"	/* stat */
 
 #if !defined(S_ISDIR)
 	#define S_ISDIR(x)	((x)&S_IFDIR)
@@ -1095,7 +1094,7 @@ BOOL DLLCALL isfullpath(const char* filename)
 /* Optionally not allowing * to match PATH_DELIM (for paths)				*/
 /****************************************************************************/
 
-BOOL DLLCALL wildmatch(const char *fname, const char *spec, BOOL path)
+BOOL DLLCALL wildmatch(const char *fname, const char *spec, BOOL path, BOOL case_sensitive)
 {
 	char *specp;
 	char *fnamep;
@@ -1127,12 +1126,14 @@ BOOL DLLCALL wildmatch(const char *fname, const char *spec, BOOL path)
 				else
 					wildend=strchr(fnamep, 0);
 				for(;wildend >= fnamep;wildend--) {
-					if(wildmatch(wildend, specp, path))
+					if(wildmatch(wildend, specp, path, case_sensitive))
 						return(TRUE);
 				}
 				return(FALSE);
 			default:
-				if(*specp != *fnamep)
+				if(case_sensitive && *specp != *fnamep)
+					return(FALSE);
+				if((!case_sensitive) && toupper(*specp) != toupper(*fnamep))
 					return(FALSE);
 		}
 		if(!(*specp && *fnamep))
@@ -1142,6 +1143,8 @@ BOOL DLLCALL wildmatch(const char *fname, const char *spec, BOOL path)
 		specp++;
 	if(*specp==*fnamep)
 		return(TRUE);
+	if((!case_sensitive) && toupper(*specp) == toupper(*fnamep))
+		return(TRUE);
 	return(FALSE);
 }
 
@@ -1150,22 +1153,7 @@ BOOL DLLCALL wildmatch(const char *fname, const char *spec, BOOL path)
 /****************************************************************************/
 BOOL DLLCALL wildmatchi(const char *fname, const char *spec, BOOL path)
 {
-	char* s1;
-	char* s2;
-	BOOL result;
-
-	if((s1=strdup(fname))==NULL)
-		return(FALSE);
-	if((s2=strdup(spec))==NULL) {
-		free(s1);
-		return(FALSE);
-	}
-	strupr(s1);
-	strupr(s2);
-	result = wildmatch(s1, s2, path);
-	free(s1);
-	free(s2);
-	return(result);
+	return wildmatch(fname, spec, path, /* case_sensitive: */FALSE);
 }
 
 /****************************************************************************/
diff --git a/src/xpdev/dirwrap.h b/src/xpdev/dirwrap.h
index 4bb505b22fdecd9691515fbf79e54c9988b1a5d1..8c4ba2381bb9ec81695d7c72c15f43afc6c935b2 100644
--- a/src/xpdev/dirwrap.h
+++ b/src/xpdev/dirwrap.h
@@ -1,7 +1,4 @@
 /* Directory system-call wrappers */
-// vi: tabstop=4
-
-/* $Id: dirwrap.h,v 1.55 2019/09/20 08:59:34 rswindell Exp $ */
 
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
@@ -16,21 +13,9 @@
  * See the GNU Lesser General Public License for more details: lgpl.txt or	*
  * http://www.fsf.org/copyleft/lesser.html									*
  *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
- *																			*
  * For Synchronet coding style and modification guidelines, see				*
  * http://www.synchro.net/source.html										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
@@ -54,13 +39,14 @@
 extern "C" {
 #endif
 
+#define ALLFILES "*"	/* matches all files in a directory */
+
 /****************/
 /* RTL-specific */
 /****************/
 
 #if defined(__unix__)
 
-	#define ALLFILES "*"	/* matches all files in a directory */
 	#include <sys/types.h>
 	#include <sys/stat.h>
 	#include <glob.h>		/* POSIX.2 directory pattern matching function */
@@ -81,7 +67,6 @@ extern "C" {
 
 	#include <direct.h>		/* mkdir() */
 
-	#define ALLFILES "*.*"	/* matches all files in a directory */
 	#ifdef __WATCOMC__
 		#define MKDIR(dir)		mkdir(dir)
 	#else
@@ -236,7 +221,7 @@ DLLEXPORT ulong		DLLCALL getfreediskspace(const char* path, ulong unit);
 DLLEXPORT uint64_t	DLLCALL getfilesizetotal(const char *path);
 DLLEXPORT long		DLLCALL delfiles(const char *inpath, const char *spec, size_t keep);
 DLLEXPORT char*		DLLCALL backslash(char* path);
-DLLEXPORT BOOL 		DLLCALL wildmatch(const char *fname, const char *spec, BOOL path);
+DLLEXPORT BOOL 		DLLCALL wildmatch(const char *fname, const char *spec, BOOL path, BOOL case_sensitive);
 DLLEXPORT BOOL 		DLLCALL wildmatchi(const char *fname, const char *spec, BOOL path);
 DLLEXPORT int		DLLCALL	mkpath(const char* path);
 
diff --git a/src/xpdev/gen_defs.h b/src/xpdev/gen_defs.h
index ecbd93a3a62b566d371bca466a63a5ae06f39be3..b516a8e0fbb1cba2e7e79694657514929b858197 100644
--- a/src/xpdev/gen_defs.h
+++ b/src/xpdev/gen_defs.h
@@ -273,6 +273,9 @@ typedef intmax_t	intptr_t;
 typedef int32_t         time32_t;
 
 #if defined(_WIN32)
+#  if defined _MSC_VER && !defined _FILE_OFFSET_BITS
+#    define _FILE_OFFSET_BITS 64
+#  endif
 #  if defined(_FILE_OFFSET_BITS) && (_FILE_OFFSET_BITS==64)
 #    define off_t       int64_t
 #    define PRIdOFF     PRId64
@@ -459,22 +462,22 @@ typedef struct {
 #define IS_DIGIT(c)						isdigit((unsigned char)(c))
 #define IS_HEXDIGIT(c)					isxdigit((unsigned char)(c))
 #define IS_OCTDIGIT(c)					((c) >= '0' && (c) <= '7')
-#define SKIP_WHITESPACE(p)              while(*(p) && IS_WHITESPACE(*(p)))        (p)++;
-#define FIND_WHITESPACE(p)              while(*(p) && !IS_WHITESPACE(*(p)))       (p)++;
-#define SKIP_CHAR(p,c)                  while(*(p)==c)                            (p)++;
-#define FIND_CHAR(p,c)                  while(*(p) && *(p)!=c)                    (p)++;
-#define SKIP_CHARSET(p,s)               while(*(p) && strchr(s,*(p))!=NULL)       (p)++;
-#define FIND_CHARSET(p,s)               while(*(p) && strchr(s,*(p))==NULL)       (p)++;
+#define SKIP_WHITESPACE(p)              while((p) && *(p) && IS_WHITESPACE(*(p)))        (p)++;
+#define FIND_WHITESPACE(p)              while((p) && *(p) && !IS_WHITESPACE(*(p)))       (p)++;
+#define SKIP_CHAR(p,c)                  while((p) && *(p)==c)                            (p)++;
+#define FIND_CHAR(p,c)                  while((p) && *(p) && *(p)!=c)                    (p)++;
+#define SKIP_CHARSET(p,s)               while((p) && *(p) && strchr(s,*(p))!=NULL)       (p)++;
+#define FIND_CHARSET(p,s)               while((p) && *(p) && strchr(s,*(p))==NULL)       (p)++;
 #define SKIP_CRLF(p)					SKIP_CHARSET(p, "\r\n")
 #define FIND_CRLF(p)					FIND_CHARSET(p, "\r\n")
-#define SKIP_ALPHA(p)                   while(*(p) && IS_ALPHA(*(p)))             (p)++;
-#define FIND_ALPHA(p)                   while(*(p) && !IS_ALPHA(*(p)))            (p)++;
-#define SKIP_ALPHANUMERIC(p)            while(*(p) && IS_ALPHANUMERIC(*(p)))      (p)++;
-#define FIND_ALPHANUMERIC(p)            while(*(p) && !IS_ALPHANUMERIC(*(p)))     (p)++;
-#define SKIP_DIGIT(p)                   while(*(p) && IS_DIGIT(*(p)))             (p)++;
-#define FIND_DIGIT(p)                   while(*(p) && !IS_DIGIT(*(p)))            (p)++;
-#define SKIP_HEXDIGIT(p)                while(*(p) && IS_HEXDIGIT(*(p)))          (p)++;
-#define FIND_HEXDIGIT(p)                while(*(p) && !IS_HEXDIGIT(*(p)))         (p)++;
+#define SKIP_ALPHA(p)                   while((p) && *(p) && IS_ALPHA(*(p)))             (p)++;
+#define FIND_ALPHA(p)                   while((p) && *(p) && !IS_ALPHA(*(p)))            (p)++;
+#define SKIP_ALPHANUMERIC(p)            while((p) && *(p) && IS_ALPHANUMERIC(*(p)))      (p)++;
+#define FIND_ALPHANUMERIC(p)            while((p) && *(p) && !IS_ALPHANUMERIC(*(p)))     (p)++;
+#define SKIP_DIGIT(p)                   while((p) && *(p) && IS_DIGIT(*(p)))             (p)++;
+#define FIND_DIGIT(p)                   while((p) && *(p) && !IS_DIGIT(*(p)))            (p)++;
+#define SKIP_HEXDIGIT(p)                while((p) && *(p) && IS_HEXDIGIT(*(p)))          (p)++;
+#define FIND_HEXDIGIT(p)                while((p) && *(p) && !IS_HEXDIGIT(*(p)))         (p)++;
 
 #define HEX_CHAR_TO_INT(ch) 			(((ch)&0xf)+(((ch)>>6)&1)*9)
 #define DEC_CHAR_TO_INT(ch)				((ch)&0xf)
diff --git a/src/xpdev/ini_file.c b/src/xpdev/ini_file.c
index b347d72cba2e7e65280e88cdfc9add2faf7e99f2..c20f5a700e238dac2015dd21915b0690ddd825e5 100644
--- a/src/xpdev/ini_file.c
+++ b/src/xpdev/ini_file.c
@@ -312,6 +312,9 @@ BOOL iniSectionExists(str_list_t list, const char* section)
 {
 	size_t	i;
 
+	if(list==NULL)
+		return(FALSE);
+
 	if(section==ROOT_SECTION)
 		return(TRUE);
 
diff --git a/src/xpdev/str_list.c b/src/xpdev/str_list.c
index 21fb5819b116a2c4bfb4e44afcf6635c0b460761..d374157d1bd2c66f1c9b76994debf22d0fbefbfb 100644
--- a/src/xpdev/str_list.c
+++ b/src/xpdev/str_list.c
@@ -1,9 +1,5 @@
-/* str_list.c */
-
 /* Functions to deal with NULL-terminated string lists */
 
-/* $Id: str_list.c,v 1.61 2020/05/26 02:46:15 rswindell Exp $ */
-
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
  * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
@@ -17,21 +13,9 @@
  * See the GNU Lesser General Public License for more details: lgpl.txt or	*
  * http://www.fsf.org/copyleft/lesser.html									*
  *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
- *																			*
  * For Synchronet coding style and modification guidelines, see				*
  * http://www.synchro.net/source.html										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
@@ -167,6 +151,28 @@ char* strListRemove(str_list_t* list, size_t index)
 	return(str);
 }
 
+// Remove without realloc
+char* strListFastRemove(str_list_t list, size_t index)
+{
+	char*	str;
+	size_t	i;
+	size_t	count;
+
+	count = strListCount(list);
+
+	if(index == STR_LIST_LAST_INDEX && count)
+		index = count-1;
+
+	if(index >= count)	/* invalid index, do nothing */
+		return NULL;
+
+	str = list[index];
+	for(i = index; i < count; i++)
+		list[i] = list[i + 1];
+
+	return str;
+}
+
 BOOL strListDelete(str_list_t* list, size_t index)
 {
 	char*	str;
@@ -179,6 +185,18 @@ BOOL strListDelete(str_list_t* list, size_t index)
 	return(TRUE);
 }
 
+BOOL strListFastDelete(str_list_t list, size_t index)
+{
+	char*	str;
+
+	if((str=strListFastRemove(list, index))==NULL)
+		return(FALSE);
+
+	free(str);
+
+	return(TRUE);
+}
+
 char* strListReplace(const str_list_t list, size_t index, const char* str)
 {
 	char*	buf;
@@ -818,3 +836,35 @@ int strListDedupe(str_list_t* list, BOOL case_sensitive)
 	}
 	return i;
 }
+
+int strListDeleteBlanks(str_list_t* list)
+{
+	size_t		i;
+
+	if(list == NULL || *list == NULL)
+		return 0;
+
+	for(i = 0; (*list)[i] != NULL; ) {
+		if((*list)[i][0] == '\0')
+			strListDelete(list, i);
+		else
+			i++;
+	}
+	return i;
+}
+
+int strListFastDeleteBlanks(str_list_t list)
+{
+	size_t		i;
+
+	if(list == NULL || *list == NULL)
+		return 0;
+
+	for(i = 0; list[i] != NULL; ) {
+		if(list[i][0] == '\0')
+			strListFastDelete(list, i);
+		else
+			i++;
+	}
+	return i;
+}
diff --git a/src/xpdev/str_list.h b/src/xpdev/str_list.h
index 0bafae09842950d01c43bc016710e3f5076079c9..a5994b359efe7f4c14c6b49ff7b4ab374b864a34 100644
--- a/src/xpdev/str_list.h
+++ b/src/xpdev/str_list.h
@@ -1,9 +1,5 @@
-/* str_list.h */
-
 /* Functions to deal with NULL-terminated string lists */
 
-/* $Id: str_list.h,v 1.32 2020/04/24 07:02:17 rswindell Exp $ */
-
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
  * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
@@ -17,21 +13,9 @@
  * See the GNU Lesser General Public License for more details: lgpl.txt or	*
  * http://www.fsf.org/copyleft/lesser.html									*
  *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
- *																			*
  * For Synchronet coding style and modification guidelines, see				*
  * http://www.synchro.net/source.html										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
@@ -86,9 +70,11 @@ DLLEXPORT char*			strListInsertFormat(str_list_t* list, size_t index, const char
 
 /* Remove a string at a specific index */
 DLLEXPORT char*			strListRemove(str_list_t*, size_t index);
+DLLEXPORT char*			strListFastRemove(str_list_t, size_t index);
 
 /* Remove and free a string at a specific index */
 DLLEXPORT BOOL			strListDelete(str_list_t*, size_t index);
+DLLEXPORT BOOL			strListFastDelete(str_list_t, size_t index);
 
 /* Replace a string at a specific index */
 DLLEXPORT char*			strListReplace(const str_list_t, size_t index, const char* str);
@@ -113,7 +99,7 @@ DLLEXPORT BOOL			strListSwap(const str_list_t, size_t index1, size_t index2);
 #define		strListPush(list, str)	strListAppend(list, str, STR_LIST_LAST_INDEX)
 #define		strListPop(list)		strListRemove(list, STR_LIST_LAST_INDEX)
 
-/* Add to an exiting or new string list by splitting specified string (str) */
+/* Add to an existing or new string list by splitting specified string (str) */
 /* into multiple strings, separated by one of the delimit characters */
 DLLEXPORT str_list_t	strListSplit(str_list_t*, char* str, const char* delimit);
 
@@ -168,6 +154,9 @@ DLLEXPORT int			strListTruncateStrings(str_list_t, const char* set);
 DLLEXPORT int			strListStripStrings(str_list_t, const char* set);
 /* Remove duplicate strings from list, return the new list length */
 DLLEXPORT int			strListDedupe(str_list_t*, BOOL case_sensitive);
+/* Remove blank strings from list, return the new list length */
+DLLEXPORT int			strListDeleteBlanks(str_list_t*);
+DLLEXPORT int			strListFastDeleteBlanks(str_list_t);
 
 /************/
 /* File I/O */
diff --git a/src/xpdev/xpdatetime.c b/src/xpdev/xpdatetime.c
index de6363d88f140d5e8b0680d4d4515be86fef15a8..c5ebcf1eb15df2046e4f8ea219e7ab19cd421470 100644
--- a/src/xpdev/xpdatetime.c
+++ b/src/xpdev/xpdatetime.c
@@ -171,6 +171,20 @@ xpDateTime_t DLLCALL time_to_xpDateTime(time_t ti, xpTimeZone_t zone)
 		,zone==xpTimeZone_LOCAL ? xpTimeZone_local() : zone);
 }
 
+xpDate_t DLLCALL time_to_xpDate(time_t ti)
+{
+	xpDate_t never;
+	struct tm tm;
+
+	ZERO_VAR(never);
+	ZERO_VAR(tm);
+	if(localtime_r(&ti,&tm)==NULL)
+		return never;
+
+	return xpDateTime_create(1900+tm.tm_year,1+tm.tm_mon,tm.tm_mday
+		,tm.tm_hour,tm.tm_min,(float)tm.tm_sec, /* zone: */0).date;
+}
+
 xpDateTime_t DLLCALL gmtime_to_xpDateTime(time_t ti)
 {
 	xpDateTime_t	never;
diff --git a/src/xpdev/xpdatetime.h b/src/xpdev/xpdatetime.h
index 942a78413c08b0d275f64bfaaa1549a9e725022d..bd9dc0fd2c9f4b203bed53ea0ac98bc777f22b65 100644
--- a/src/xpdev/xpdatetime.h
+++ b/src/xpdev/xpdatetime.h
@@ -79,6 +79,7 @@ DLLEXPORT xpDateTime_t	DLLCALL xpDateTime_create(unsigned year, unsigned month,
 DLLEXPORT xpDateTime_t	DLLCALL xpDateTime_now(void);
 DLLEXPORT time_t		DLLCALL xpDateTime_to_time(xpDateTime_t);
 DLLEXPORT time_t		DLLCALL xpDateTime_to_localtime(xpDateTime_t);
+DLLEXPORT xpDate_t		DLLCALL time_to_xpDate(time_t);
 DLLEXPORT xpDateTime_t	DLLCALL time_to_xpDateTime(time_t, xpTimeZone_t);
 DLLEXPORT xpDateTime_t	DLLCALL gmtime_to_xpDateTime(time_t);
 DLLEXPORT xpTimeZone_t	DLLCALL xpTimeZone_local(void);
diff --git a/text/menu/sysxfer.asc b/text/menu/sysxfer.asc
index 895c330e0c149f0559a6727a652655433165a84e..0c26ee5554efd615b217aa8e74e1437ffbccb7e3 100644
--- a/text/menu/sysxfer.asc
+++ b/text/menu/sysxfer.asc
@@ -5,10 +5,7 @@ PUT [s]         Direct upload file s to any drive or directory (remote)
 GET [s]         Direct download file s from any drive or directory (remote)
 OLD           *	Remove, Edit, or Move files not downloaded since new-scan date
 OLDUL         *	Remove, Edit, or Move files uploaded before new-scan date
-CLOSE         *	Search for and optionally close open file records
-ALTUL [x]	Perform uploads using alternate file path number x
 UPLOAD        *	Bulk local upload - add files on disk to database
-RESORT        *	Re-sort files by sort value and compress empty slots
 OFFLINE       *	Remove, Edit, or Move files in database that aren't on disk
 
 * Commands can be followed by LIB or ALL to specify the action to take place
diff --git a/text/menu/transfer.msg b/text/menu/transfer.msg
index 44e427fd7c294e11a258dba38ea343bf91daa6a3..5dd9070b88836a7214ab37c09370f714692b2a8a 100644
--- a/text/menu/transfer.msg
+++ b/text/menu/transfer.msg
@@ -9,9 +9,9 @@
  �� hy[ ] c/# ncSelect library   b�  hy& ncFile scan config
    hyD ncDownload file�b���  hyR ncRemove/edit file
    hyU ncUpload file�b����������4 hwGo to nb����������  hyI ncInformation
-  hy/D ncDownload from user   b���  hyV ncView file contents
-  hy/U ncUpload to user�b� hyQ ncMain/Message section  b� hy/L ncNode activity
-   hyZ ncUpload to sysop�b� hyC ncChat section�b� hy^K ncCtrl-key menu
-   hyB ncBatch/Bi-dir xfers   b� hyT ncTemp dir/Archive cmds b�  hyO ncLogoff BBS (or hy/Onc)
+   hyZ ncUpload to sysop      b���  hyV ncView file contents
+   b������������������������hy Q ncMain/Message section  b� hy/L ncNode activity
+   hyB ncBatch/Bi-dir xfers   b� hyC ncChat section�b� hy^K ncCtrl-key menu
+                          b� hyT ncTemp dir/Archive cmds b�  hyO ncLogoff BBS (or hy/Onc)
 b ����  @SYSONLY@hy!nc Sysop Menu@SYSONLY@
 @HOT:OFF@@INCLUDE:%zmenu/tail.msg@
\ No newline at end of file