Add support for offline files support, remote storage, and Async I/O force operations...
authorAlexander Bokovoy <ab@samba.org>
Wed, 16 Jan 2008 09:17:03 +0000 (12:17 +0300)
committerAlexander Bokovoy <ab@samba.org>
Wed, 16 Jan 2008 09:17:03 +0000 (12:17 +0300)
Offline files support and remote storage are for allowing communication with
backup and archiving tools that mark files moved to a tape library as offline.
We translate this info into corresponding CIFS offline file attribute and
mark an exported volume as remote storage.

Async I/O force is to allow selective redirection of I/O operations to asynchronous
processing in case it is viable at VFS module discretion. It is needed for
proper handling of offline files as performing regular I/O on offline file will
block smbd.

Signed-off-by: Alexander Bokovoy <ab@samba.org>
source/include/vfs.h
source/include/vfs_macros.h
source/modules/vfs_default.c
source/smbd/dmapi.c
source/smbd/dosmode.c
source/smbd/reply.c
source/smbd/trans2.c

index 0be3886227895401d44e166ea7b8f1bd119e4508..be3cd91520aa16c75c8324b7ee08a05a42bf7a8f 100644 (file)
@@ -70,6 +70,7 @@
  * timestamp resolition. JRA. */
 /* Changed to version21 to add chflags operation -- jpeach */
 /* Changed to version22 to add lchown operation -- jra */
+/* Additional change: add operations for offline files and remote storage volume abstraction -- ab*/
 /* Leave at 22 - not yet released. But change set_nt_acl to return an NTSTATUS. jra. */
 /* Leave at 22 - not yet released. Add file_id_create operation. --metze */
 /* Leave at 22 - not yet released. Change all BOOL parameters (int) to bool. jra. */
@@ -257,6 +258,12 @@ typedef enum _vfs_op_type {
        SMB_VFS_OP_AIO_ERROR,
        SMB_VFS_OP_AIO_FSYNC,
        SMB_VFS_OP_AIO_SUSPEND,
+        SMB_VFS_OP_AIO_FORCE,
+
+       /* offline operations */
+       SMB_VFS_OP_IS_OFFLINE,
+       SMB_VFS_OP_SET_OFFLINE,
+       SMB_VFS_OP_IS_REMOTESTORAGE,
 
        /* This should always be last enum value */
        
@@ -405,6 +412,12 @@ struct vfs_ops {
                int (*aio_error_fn)(struct vfs_handle_struct *handle, struct files_struct *fsp, SMB_STRUCT_AIOCB *aiocb);
                int (*aio_fsync)(struct vfs_handle_struct *handle, struct files_struct *fsp, int op, SMB_STRUCT_AIOCB *aiocb);
                int (*aio_suspend)(struct vfs_handle_struct *handle, struct files_struct *fsp, const SMB_STRUCT_AIOCB * const aiocb[], int n, const struct timespec *timeout);
+               bool (*aio_force)(struct vfs_handle_struct *handle, struct files_struct *fsp);
+               
+               /* offline operations */
+               int (*is_offline)(struct vfs_handle_struct *handle, struct connection_struct *conn, const char *path, SMB_STRUCT_STAT *sbuf, bool *offline);
+               int (*set_offline)(struct vfs_handle_struct *handle, struct connection_struct *conn, const char *path);
+               bool (*is_remotestorage)(struct vfs_handle_struct *handle, struct connection_struct *conn, const char *path);
 
        } ops;
 
@@ -526,6 +539,12 @@ struct vfs_ops {
                struct vfs_handle_struct *aio_error;
                struct vfs_handle_struct *aio_fsync;
                struct vfs_handle_struct *aio_suspend;
+               struct vfs_handle_struct *aio_force;
+               
+               /* offline operations */
+               struct vfs_handle_struct *is_offline;
+               struct vfs_handle_struct *set_offline;
+               struct vfs_handle_struct *is_remotestorage;
        } handles;
 };
 
index 9232e94a421f98008d8bd573df26ea88fb2a1486..dbba7813771c55f2aaebd6d9152cb062c16f7395 100644 (file)
 #define SMB_VFS_AIO_ERROR(fsp,aiocb) ((fsp)->conn->vfs.ops.aio_error_fn((fsp)->conn->vfs.handles.aio_error,(fsp),(aiocb)))
 #define SMB_VFS_AIO_FSYNC(fsp,op,aiocb) ((fsp)->conn->vfs.ops.aio_fsync((fsp)->conn->vfs.handles.aio_fsync,(fsp),(op),(aiocb)))
 #define SMB_VFS_AIO_SUSPEND(fsp,aiocb,n,ts) ((fsp)->conn->vfs.ops.aio_suspend((fsp)->conn->vfs.handles.aio_suspend,(fsp),(aiocb),(n),(ts)))
+#define SMB_VFS_AIO_FORCE(fsp) ((fsp)->conn->vfs.ops.aio_force((fsp)->conn->vfs.handles.aio_force,(fsp)))
+
+/* Offline operations */
+#define SMB_VFS_IS_OFFLINE(conn,path,sbuf,offline) ((conn)->vfs.ops.is_offline((conn)->vfs.handles.is_offline,(conn),(path),(sbuf),(offline)))
+#define SMB_VFS_SET_OFFLINE(conn,path) ((conn)->vfs.ops.set_offline((conn)->vfs.handles.set_offline,(conn),(path)))
+#define SMB_VFS_IS_REMOTESTORAGE(conn,path) ((conn)->vfs.ops.is_remotestorage((conn)->vfs.handles.is_remotestorage,(conn),(path)))
 
 /*******************************************************************
  Don't access conn->vfs_opaque.ops directly!!!
 #define SMB_VFS_OPAQUE_AIO_ERROR(fsp,aiocb) ((fsp)->conn->vfs_opaque.ops.aio_error_fn((fsp)->conn->vfs_opaque.handles.aio_error,(fsp),(aiocb)))
 #define SMB_VFS_OPAQUE_AIO_FSYNC(fsp,op,aiocb) ((fsp)->conn->vfs_opaque.ops.aio_fsync((fsp)->conn->vfs_opaque.handles.aio_fsync,(fsp),(op),(aiocb)))
 #define SMB_VFS_OPAQUE_AIO_SUSPEND(fsp,aiocb,n,ts) ((fsp)->conn->vfs_opaque.ops.aio_suspend((fsp)->conn->vfs_opaque.handles.aio_suspend,(fsp),(aiocb),(n),(ts)))
+#define SMB_VFS_OPAQUE_AIO_FORCE(fsp) ((fsp)->conn->vfs_opaque.ops.aio_force((fsp)->conn->vfs_opaque.handles.aio_force,(fsp)))
+
+/* Offline operations */
+#define SMB_VFS_OPAQUE_IS_OFFLINE(conn,path,sbuf,offline) ((conn)->vfs_opaque.ops.is_offline((conn)->vfs_opaque.handles.is_offline,(conn),(path),(sbuf),(offline)))
+#define SMB_VFS_OPAQUE_SET_OFFLINE(conn,path) ((conn)->vfs_opaque.ops.set_offline((conn)->vfs_opaque.handles.set_offline,(conn),(path)))
+#define SMB_VFS_OPAQUE_IS_REMOTESTORAGE(conn,path) ((conn)->vfs_opaque.ops.is_remotestorage((conn)->vfs_opaque.handles.is_remotestorage,(conn),(path)))
 
 /*******************************************************************
  Don't access handle->vfs_next.ops.* directly!!!
 #define SMB_VFS_NEXT_AIO_ERROR(handle,fsp,aiocb) ((handle)->vfs_next.ops.aio_error_fn((handle)->vfs_next.handles.aio_error,(fsp),(aiocb)))
 #define SMB_VFS_NEXT_AIO_FSYNC(handle,fsp,op,aiocb) ((handle)->vfs_next.ops.aio_fsync((handle)->vfs_next.handles.aio_fsync,(fsp),(op),(aiocb)))
 #define SMB_VFS_NEXT_AIO_SUSPEND(handle,fsp,aiocb,n,ts) ((handle)->vfs_next.ops.aio_suspend((handle)->vfs_next.handles.aio_suspend,(fsp),(aiocb),(n),(ts)))
+#define SMB_VFS_NEXT_AIO_FORCE(handle,fsp) ((handle)->vfs_next.ops.aio_force((handle)->vfs_next.handles.aio_force,(fsp)))
+
+/* Offline operations */
+#define SMB_VFS_NEXT_IS_OFFLINE(conn,path,sbuf,offline) ((conn)->vfs_next.ops.is_offline((conn)->vfs_next.handles.is_offline,(conn),(path),(sbuf),(offline)))
+#define SMB_VFS_NEXT_SET_OFFLINE(conn,path) ((conn)->vfs_next.ops.set_offline((conn)->vfs_next.handles.set_offline,(conn),(path)))
+#define SMB_VFS_NEXT_IS_REMOTESTORAGE(conn,path) ((conn)->vfs_next.ops.is_remotestorage((conn)->vfs_next.handles.is_remotestorage,(conn),(path)))
 
 #endif /* _VFS_MACROS_H */
index e21136ccee9e0a10ef5464f0b54a87deb0a9dd40..3e78c69a5e6412ce3bf75372e5e4c134aba2f10e 100644 (file)
@@ -1225,6 +1225,38 @@ static int vfswrap_aio_suspend(struct vfs_handle_struct *handle, struct files_st
        return sys_aio_suspend(aiocb, n, timeout);
 }
 
+static int vfswrap_aio_force(struct vfs_handle_struct *handle, struct files_struct *fsp)
+{
+       return False;
+}
+
+static int vfswrap_is_offline(struct vfs_handle_struct *handle, struct connection_struct *conn, const char *path, SMB_STRUCT_STAT *sbuf, bool *offline)
+{
+       if (ISDOT(path) || ISDOTDOT(path)) {
+               return 1;
+       }
+       
+       if (!lp_dmapi_support(SNUM(conn)) || !dmapi_have_session()) {
+               return -1;
+       }
+       
+       *offline = (dmapi_file_flags(path) & FILE_ATTRIBUTE_OFFLINE) != 0;
+       return 0;
+}
+
+static int vfswrap_set_offline(struct vfs_handle_struct *handle, struct connection_struct *conn, const char *path)
+{
+       /* We don't know how to set offline bit by default, needs to be overriden in the vfs modules */
+       return -1;
+}
+
+static bool vfswrap_is_remotestorage(struct vfs_handle_struct *handle, struct connection_struct *conn, const char *path)
+{
+       /* We don't know how to detect that volume is remote storage. VFS modules should redefine it. */
+       return False;
+}
+
+
 static vfs_op_tuple vfs_default_ops[] = {
 
        /* Disk operations */
@@ -1442,6 +1474,16 @@ static vfs_op_tuple vfs_default_ops[] = {
        {SMB_VFS_OP(vfswrap_aio_suspend),SMB_VFS_OP_AIO_SUSPEND,
         SMB_VFS_LAYER_OPAQUE},
 
+       {SMB_VFS_OP(vfswrap_aio_force), SMB_VFS_OP_AIO_FORCE,
+        SMB_VFS_LAYER_OPAQUE},
+       
+       {SMB_VFS_OP(vfswrap_is_offline),SMB_VFS_OP_IS_OFFLINE,
+        SMB_VFS_LAYER_OPAQUE},
+       {SMB_VFS_OP(vfswrap_set_offline),SMB_VFS_OP_SET_OFFLINE,
+        SMB_VFS_LAYER_OPAQUE},
+       {SMB_VFS_OP(vfswrap_is_remotestorage),SMB_VFS_OP_IS_REMOTESTORAGE,
+        SMB_VFS_LAYER_OPAQUE},
+       
        /* Finish VFS operations definition */
 
        {SMB_VFS_OP(NULL),              SMB_VFS_OP_NOOP,
index 05e9138ea97920bad5e7b51e6debf57fcf4d3e84..620baf199e42cf8634af6a8e4f9b50b9b8f640ef 100644 (file)
@@ -46,7 +46,7 @@ bool dmapi_have_session(void) { return False; }
 #define DMAPI_SESSION_NAME "samba"
 #define DMAPI_TRACE 10
 
-static dm_sessid_t dmapi_session = DM_NO_SESSION;
+static dm_sessid_t samba_dmapi_session = DM_NO_SESSION;
 
 /* Initialise the DMAPI interface. Make sure that we only end up initialising
  * once per process to avoid resource leaks across different DMAPI
@@ -75,7 +75,7 @@ static int init_dmapi_service(void)
 
 bool dmapi_have_session(void)
 {
-       return dmapi_session != DM_NO_SESSION;
+       return samba_dmapi_session != DM_NO_SESSION;
 }
 
 static dm_sessid_t *realloc_session_list(dm_sessid_t * sessions, int count)
@@ -110,7 +110,7 @@ int dmapi_init_session(void)
         */
        SMB_WARN(getuid() == 0, "dmapi_init_session must be called as root");
 
-       dmapi_session = DM_NO_SESSION;
+       samba_dmapi_session = DM_NO_SESSION;
        if (init_dmapi_service() < 0) {
                return -1;
        }
@@ -139,7 +139,7 @@ retry:
                err = dm_query_session(sessions[i], sizeof(buf), buf, &buflen);
                buf[sizeof(buf) - 1] = '\0';
                if (err == 0 && strcmp(DMAPI_SESSION_NAME, buf) == 0) {
-                       dmapi_session = sessions[i];
+                       samba_dmapi_session = sessions[i];
                        DEBUGADD(DMAPI_TRACE,
                                ("attached to existing DMAPI session "
                                 "named '%s'\n", buf));
@@ -150,16 +150,15 @@ retry:
        TALLOC_FREE(sessions);
 
        /* No session already defined. */
-       if (dmapi_session == DM_NO_SESSION) {
-               err = dm_create_session(DM_NO_SESSION,
-                                       CONST_DISCARD(char *,
-                                                     DMAPI_SESSION_NAME),
-                                       &dmapi_session);
+       if (samba_dmapi_session == DM_NO_SESSION) {
+               err = dm_create_session(DM_NO_SESSION, 
+                                       CONST_DISCARD(char *, DMAPI_SESSION_NAME),
+                                       &samba_dmapi_session);
                if (err < 0) {
                        DEBUGADD(DMAPI_TRACE,
                                ("failed to create new DMAPI session: %s\n",
                                strerror(errno)));
-                       dmapi_session = DM_NO_SESSION;
+                       samba_dmapi_session = DM_NO_SESSION;
                        return -1;
                }
 
@@ -185,22 +184,22 @@ static int reattach_dmapi_session(void)
        char    buf[DM_SESSION_INFO_LEN];
        size_t  buflen;
 
-       if (dmapi_session != DM_NO_SESSION ) {
+       if (samba_dmapi_session != DM_NO_SESSION ) {
                become_root();
 
                /* NOTE: On Linux, this call opens /dev/dmapi, costing us a
                 * file descriptor. Ideally, we would close this when we fork.
                 */
                if (init_dmapi_service() < 0) {
-                       dmapi_session = DM_NO_SESSION;
+                       samba_dmapi_session = DM_NO_SESSION;
                        unbecome_root();
                        return -1;
                }
 
-               if (dm_query_session(dmapi_session, sizeof(buf),
+               if (dm_query_session(samba_dmapi_session, sizeof(buf),
                            buf, &buflen) < 0) {
                        /* Session is stale. Disable DMAPI. */
-                       dmapi_session = DM_NO_SESSION;
+                       samba_dmapi_session = DM_NO_SESSION;
                        unbecome_root();
                        return -1;
                }
@@ -214,33 +213,42 @@ static int reattach_dmapi_session(void)
        return 0;
 }
 
-uint32 dmapi_file_flags(const char * const path)
+/* If a DMAPI session has been initialised, then we need to make sure
+ * we are attached to it and have the correct privileges. This is
+ * necessary to be able to do DMAPI operations across a fork(2). If
+ * it fails, there is no likelihood of that failure being transient.
+ *
+ * Note that this use of the static attached flag relies on the fact
+ * that dmapi_file_flags() is never called prior to forking the
+ * per-client server process.
+ */
+const void * dmapi_get_current_session(void) 
 {
        static int attached = 0;
+       if (dmapi_have_session() && !attached) {
+               attached++;
+               if (reattach_dmapi_session() < 0) {
+                       return DM_NO_SESSION;
+               }
+       }
+       return &samba_dmapi_session;
+}
 
+uint32 dmapi_file_flags(const char * const path)
+{
+       dm_sessid_t dmapi_session;
        int             err;
        dm_eventset_t   events = {0};
        uint            nevents;
 
-       void    *dm_handle;
-       size_t  dm_handle_len;
+       void    *dm_handle = NULL;
+       size_t  dm_handle_len = 0;
 
        uint32  flags = 0;
 
-       /* If a DMAPI session has been initialised, then we need to make sure
-        * we are attached to it and have the correct privileges. This is
-        * necessary to be able to do DMAPI operations across a fork(2). If
-        * it fails, there is no liklihood of that failure being transient.
-        *
-        * Note that this use of the static attached flag relies on the fact
-        * that dmapi_file_flags() is never called prior to forking the
-        * per-client server process.
-        */
-       if (dmapi_have_session() && !attached) {
-               attached++;
-               if (reattach_dmapi_session() < 0) {
-                       return 0;
-               }
+       dmapi_session = *(dm_sessid_t*) dmapi_get_current_session();
+       if (dmapi_session == DM_NO_SESSION) {
+               return 0;
        }
 
        /* AIX has DMAPI but no POSIX capablities support. In this case,
@@ -313,4 +321,5 @@ done:
        return flags;
 }
 
+
 #endif /* USE_DMAPI */
index d3813f9b41b0af24c85733dd7c2f1713f666b114..2021621dfa0c94e54b74410af85c8d61184c08e1 100644 (file)
@@ -30,23 +30,6 @@ static int set_sparse_flag(const SMB_STRUCT_STAT * const sbuf)
        return 0;
 }
 
-/****************************************************************************
- Work out whether this file is offline
-****************************************************************************/
-
-static uint32 set_offline_flag(connection_struct *conn, const char *const path)
-{
-       if (ISDOT(path) || ISDOTDOT(path)) {
-               return 0;
-       }
-
-       if (!lp_dmapi_support(SNUM(conn)) || !dmapi_have_session()) {
-               return 0;
-       }
-
-       return dmapi_file_flags(path);
-}
-
 /****************************************************************************
  Change a dos mode to a unix mode.
     Base permission for files:
@@ -366,6 +349,8 @@ uint32 dos_mode_msdfs(connection_struct *conn, const char *path,SMB_STRUCT_STAT
 uint32 dos_mode(connection_struct *conn, const char *path,SMB_STRUCT_STAT *sbuf)
 {
        uint32 result = 0;
+       bool offline;
+       int ret;
 
        DEBUG(8,("dos_mode: %s\n", path));
 
@@ -395,8 +380,10 @@ uint32 dos_mode(connection_struct *conn, const char *path,SMB_STRUCT_STAT *sbuf)
                result |= dos_mode_from_sbuf(conn, path, sbuf);
        }
 
-       if (S_ISREG(sbuf->st_mode)) {
-               result |= set_offline_flag(conn, path);
+       
+       ret = SMB_VFS_IS_OFFLINE(conn, path, sbuf, &offline);
+       if (S_ISREG(sbuf->st_mode) && (ret == 0) && offline) {
+               result |= FILE_ATTRIBUTE_OFFLINE;
        }
 
        /* Optimization : Only call is_hidden_path if it's not already
@@ -432,7 +419,7 @@ int file_set_dosmode(connection_struct *conn, const char *fname,
        int mask=0;
        mode_t tmp;
        mode_t unixmode;
-       int ret = -1;
+       int ret = -1, lret = -1;
 
        /* We only allow READONLY|HIDDEN|SYSTEM|DIRECTORY|ARCHIVE here. */
        dosmode &= SAMBA_ATTRIBUTES_MASK;
@@ -505,10 +492,21 @@ int file_set_dosmode(connection_struct *conn, const char *fname,
                unixmode |= (st->st_mode & (S_IWUSR|S_IWGRP|S_IWOTH));
        }
 
-       if ((ret = SMB_VFS_CHMOD(conn,fname,unixmode)) == 0) {
-               if (!newfile) {
+       if (dosmode & FILE_ATTRIBUTE_OFFLINE) {
+               lret = SMB_VFS_SET_OFFLINE(conn, fname);
+               if (lret == -1) {
+                       DEBUG(0, ("set_dos_mode: client has asked to set "
+                                 "FILE_ATTRIBUTE_OFFLINE to %s/%s but there was "
+                                 "an error while setting it or it is not supported.\n", 
+                                 parent_dir, fname));
+               }
+       }
+
+       ret = SMB_VFS_CHMOD(conn, fname, unixmode);
+       if (ret == 0) {
+               if(!newfile || (lret != -1)) {
                        notify_fname(conn, NOTIFY_ACTION_MODIFIED,
-                               FILE_NOTIFY_CHANGE_ATTRIBUTES, fname);
+                                    FILE_NOTIFY_CHANGE_ATTRIBUTES, fname);
                }
                st->st_mode = unixmode;
                return 0;
index e2316ef12008fce523c00dcadfdbc55aad4ef193..381ddfe1517b4f83d8030af1715127a86cd6394e 100644 (file)
@@ -3329,8 +3329,12 @@ void reply_read_and_X(struct smb_request *req)
                return;
        }
 
-       if (!big_readX
-           && schedule_aio_read_and_X(conn, req, fsp, startpos, smb_maxcnt)) {
+       /* It is possible for VFS modules to selectively decide whether Async I/O should be used
+          for the file or not.
+        */
+       if ((SMB_VFS_AIO_FORCE(fsp)) &&
+           !big_readX &&
+           schedule_aio_read_and_X(conn, req, fsp, startpos, smb_maxcnt)) {
                END_PROFILE(SMBreadX);
                return;
        }
@@ -4001,13 +4005,16 @@ void reply_write_and_X(struct smb_request *req)
                nwritten = 0;
        } else {
 
-               if (req->unread_bytes == 0 &&
-                               schedule_aio_write_and_X(conn, req, fsp, data,
-                                                       startpos, numtowrite)) {
+               /* It is possible for VFS modules to selectively decide whether Async I/O
+                  should be used for the file or not.
+               */
+               if ((SMB_VFS_AIO_FORCE(fsp)) && (req->unread_bytes == 0) &&
+                   schedule_aio_write_and_X(conn, req, fsp, data, startpos,
+                                            numtowrite)) {
                        END_PROFILE(SMBwriteX);
                        return;
                }
-
+               
                nwritten = write_file(req,fsp,data,startpos,numtowrite);
        }
 
index bf6802f2a6c7e1a217daec4d07aa86efd4a2a01f..5729ab534997850fd69a3d7c7cdd3b660a6d35f3 100644 (file)
@@ -2373,8 +2373,8 @@ static void call_trans2qfsinfo(connection_struct *conn,
        const char *vname = volume_label(SNUM(conn));
        int snum = SNUM(conn);
        char *fstype = lp_fstype(SNUM(conn));
-       int quota_flag = 0;
-
+       uint32 additional_flags = 0;
+       
        if (total_params < 2) {
                reply_nterror(req, NT_STATUS_INVALID_PARAMETER);
                return;
@@ -2487,16 +2487,23 @@ cBytesSector=%u, cUnitTotal=%u, cUnitAvail=%d\n", (unsigned int)st.st_dev, (unsi
                case SMB_QUERY_FS_ATTRIBUTE_INFO:
                case SMB_FS_ATTRIBUTE_INFORMATION:
 
-
+                       additional_flags = 0;
 #if defined(HAVE_SYS_QUOTAS)
-                       quota_flag = FILE_VOLUME_QUOTAS;
+                       additional_flags |= FILE_VOLUME_QUOTAS;
 #endif
 
+                       if(lp_nt_acl_support(SNUM(conn))) {
+                               additional_flags |= FILE_PERSISTENT_ACLS;
+                       }
+                       
+                       if(SMB_VFS_IS_REMOTESTORAGE(conn, lp_pathname(SNUM(conn)))) {
+                               additional_flags |= FILE_SUPPORTS_REMOTE_STORAGE;
+                               additional_flags |= FILE_SUPPORTS_REPARSE_POINTS;
+                       }
+                       
                        SIVAL(pdata,0,FILE_CASE_PRESERVED_NAMES|FILE_CASE_SENSITIVE_SEARCH|
-                               (lp_nt_acl_support(SNUM(conn)) ? FILE_PERSISTENT_ACLS : 0)|
-                               FILE_SUPPORTS_OBJECT_IDS|
-                               FILE_UNICODE_ON_DISK|
-                               quota_flag); /* FS ATTRIBUTES */
+                               FILE_SUPPORTS_OBJECT_IDS|FILE_UNICODE_ON_DISK|
+                               additional_flags); /* FS ATTRIBUTES */
 
                        SIVAL(pdata,4,255); /* Max filename component length */
                        /* NOTE! the fstype must *not* be null terminated or win98 won't recognise it