/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "Licence").
 * You may not use this file except in compliance with the Licence.
 *
 * You can obtain a copy of the licence at RISC OS path @.^.LICENCE
 * or  http://www.riscosdev.com/lanman98/LICENCE.CDDL
 * See the Licence for the specific language governing permissions
 * and limitations under the Licence.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the Licence file. If applicable, add the
 * following below this CDDL HEADER, with the fields enclosed by
 * brackets "[]" replaced with your own identifying information:
 * Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */
 
/*
 *   Copyright 1996 Warm Silence Software Ltd.  All rights reserved.
 *   Use is subject to license terms.
 */

/*   PHBG 18/11/96: Initial version
 */

// #include "MemCheck:MemCheck.h"
#include <ctype.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include "nodebug.h"
#include "LanMan98BaseLib/strext.h"
#include "date.h"
#include "word.h"
#include "data.h"
#include "LanMan98BaseLib/error.h"
#include "LanMan98BaseLib/memory.h"
#include "LanMan98BaseLib/tcp.h"
#include "smbcmd.h"
#include "smberr.h"
#include "vc.h"
#include "mb.h"
#include "lm2.h"
#include "des.h"
#include "md4.h"
#include "md5.h"
#include "ntlm.h"
#include "ntlmssp.h"

// #include "diag.h"

#include "smb2.h"
#include "smb2fs.h"
#include "smb.h"

// #define PRINT_UID
// #define DODIFF

#define MAX(x, y) ((x) > (y) ? (x) : (y))
#define MIN(x, y) ((x) < (y) ? (x) : (y))

typedef char search_status_t[21];

typedef struct
{
    word16_t dialect;
    char security_mode;
    word16_t max_mpx_count;
    word16_t max_number_vcs;
    word32_t max_buffer_size;
    word32_t max_raw_size;
    word32_t session_key;
    word32_t capabilities;
    word32_t system_time_low;
    word32_t system_time_high;
    word16_t server_time_zone;
    char     encryption_key_length;
} nt_negprot_response_t;

typedef struct
{
    search_status_t find_buf_reserved;
    char find_buf_attr;
    word16_t find_buf_time;
    word16_t find_buf_date;
    word32_t find_buf_size;
    char find_buf_pname[13];
} dir_info_t;

typedef struct
{
    int mode;
    int dialect;
    int max_raw_read;
    int session;
    int capabilities;
    int security_mode;          /* the whole byte; mode above is two bits */
    char encryption_key[8];
} protocol_info_t;

/* Server capabilities, from the NT LM 0.12 negotiate response */
#define CAP_NT_SMBS      (0x0010)
#define CAP_STATUS32     (0x0040)
#define CAP_LARGE_READX  (0x4000)
#define CAP_LARGE_WRITEX (0x8000)
#define CAP_EXTENDED_SECURITY (0x80000000u)

/* Negotiate security mode, the two bits above the ones smb_server_s keeps */
#define SEC_SIGN_ENABLED  (0x04)
#define SEC_SIGN_REQUIRED (0x08)

/* The one status that is not a failure: the first leg of an NTLMSSP
   exchange is answered with it and the challenge. */
#define NT_MORE_PROCESSING (0xC0000016u)

/*
    A large read or write may carry far more than the buffer size agreed at
    negotiation - that is the whole point of the capability - so the buffer
    has to be grown to hold one before either is attempted.
*/
#define LARGE_XFER (60 * 1024)

#include "smbsvr.h"

struct smb_obj_s
{
#include "smbobj.h"
    dir_info_t dir_info;
};

static char *protocols[] =
{
    "PC NETWORK PROGRAM 1.0",
    "PCLAN1.0",
    "MICROSOFT NETWORKS 1.03",
    "MICROSOFT NETWORKS 3.0",
    "LANMAN1.0",
    "LM1.2X002",
    "DOS LM1.2X002",
    "DOS LANMAN2.1",
    "LANMAN2.1",
    "NT LM 0.12",
    NULL
};

static void extend_path(smb_obj_t obj)
{
    char *p;
    if(obj->path)
    {
        p = Malloc(strlen(obj->path) + strlen(obj->name) + 3);
        strcpy(p, obj->path);
        Free(obj->path);
        obj->path = p;
        p += strlen(p);
        strcpy(p, obj->name);
        p += strlen(p);
        strcpy(p, "\\");
    }
    else
    {
        // printf("Doing the root\n");
        obj->path = strdup("\\");
    }
}

static char *disc_device = "A:";
static char *printer_device = "LPT1:";
static char *rap_device = "IPC";

static void communicate(smb_server_t svr)
{
    svr->send(svr->vc);
    svr->receive(svr->vc);
}

static char *xsmb2ro(char *name)
{
    char *res;

    res = name = strdup(name);
    while(*name)
    {
        switch(*name)
        {
            case '#': *name = '?'; break;
            case '&': *name = '+'; break;
            case '@': *name = '='; break;
            case '%': *name = ';'; break;
            case '$': *name = '<'; break;
            case '^': *name = '>'; break;
            case '.': *name = '/'; break;
            case '\\': *name = '.'; break;
            case ' ': *name = 0xA0; break;
        }
        name++;
    }
    return res;
}

static char *xro2smb(char *name)
{
    char *res;

    res = name = strdup(name);
    while(*name)
    {
        switch(*name)
        {
            case '?': *name = '#'; break;
            case '+': *name = '&'; break;
            case '=': *name = '@'; break;
            case ';': *name = '%'; break;
            case '<': *name = '$'; break;
            case '>': *name = '^'; break;
            case '/': *name = '.'; break;
            case '.': *name = '\\'; break;
            case 0xA0: *name = ' '; break;
        }
        name++;
    }
    return res;
}

static void send(vc_t vc)
{
    data_t mb;

    mb = VCBuffer(vc);
    MBClearError(mb);
    MBSetFlags(mb, 0, VCFlags2(vc));
    VCSend(vc, MBMessLen(mb));
}

#if 0
static void show_error(int *id, char *tag, _kernel_oserror *err)
{
    static char buf[32];

    sprintf(buf, "LanMan98$%sError%02d", tag, (*id)++);
    VarSet(buf, err->errmess);
}
#endif

static void receive(vc_t vc)
{
    data_t mb;
    unsigned int status;
    int nt, okay, error, sub_error;

    VCReceive(vc);
    mb = VCBuffer(vc);
    /* Decode whichever scheme the response came back in, not the one that
       was asked for; the two are mapped onto the same class and code pair
       so that everything downstream of here is unaffected. */
    status = 0;
    nt = MBIsNTStatus(mb);
    if(nt)
    {
        status = MBGetNTStatus(mb);
        SMBStatusToDos(status, &error, &sub_error);
    }
    else
    {
        MBGetError(mb, &error, &sub_error);
    }
    okay = error == 0;
    if(error == 1)
    {
        switch(sub_error)
        {
            case 18:
                okay = 1;
                break;
        }
    }
    if(!okay)
    {
        VCFlush(vc);
        if(nt)
            SMBErrorStatus(status);
        else
            SMBError(error, sub_error);
    }
}

static char *name_map(char *name)
{
    char *p, *q;

    for(p = name; *p; p++)
    {
        switch(*p)
        {
            case '[': *p = '-'; break;
            case ']': *p = '-'; break;
            case ',': *p = '-'; break;
            default: *p = toupper(*p); break;
        }
    }
    p = strrchr(name, '/');
    if(p) *p++ = 0;
    for(q = strchr(name, '/'); q; q = strchr(name, '/'))
        *q = '_';
    if(name[0] == 0 || (p && *p == 0))
        Error("Name has unmappable form");
    if(strlen(name) > 8) name[8] = 0;
    if(p && strlen(p) > 3) p[3] = 0;
    if(p)
    {
        q = name + strlen(name);
        *q++ = '/';
        memmove(q, p, strlen(p)+1);
    }
    return name;
}

smb_obj_t construct(smb_server_t svr)
{
    smb_obj_t obj;

    obj = Malloc(sizeof(*obj));
    obj->svr = svr;
    obj->path = NULL;
    obj->name = NULL;
    obj->ro_name = NULL;
    obj->dirty = 0;
    return obj;
}

static smb_obj_t dup(smb_obj_t obj)
{
    smb_obj_t nobj;

    nobj = Malloc(sizeof(*nobj));
    nobj->svr = obj->svr;
    nobj->path = obj->path ? strdup(obj->path) : NULL;
    nobj->name = obj->name ? strdup(obj->name) : NULL;
    nobj->ro_name = NULL;
    nobj->length = obj->length;
    nobj->attribute = obj->attribute;
    nobj->time = obj->time;
    nobj->dirty = obj->dirty;
    nobj->dir_info = obj->dir_info;
    return nobj;
}

static void destruct(smb_obj_t obj)
{
    if(obj->name) Free(obj->name);
    if(obj->ro_name) Free(obj->ro_name);
    if(obj->path) Free(obj->path);
    Free(obj);
}

static void set_vals(smb_obj_t obj)
{
    if(obj->name)
    {
        Free(obj->name);
        obj->name = NULL;
    }
    obj->name = strdup(obj->dir_info.find_buf_pname);
    obj->time = DateDOStoSMB(Word16(obj->dir_info.find_buf_time),
                             Word16(obj->dir_info.find_buf_date));
    obj->length = Word32(obj->dir_info.find_buf_size);
    obj->attribute = obj->dir_info.find_buf_attr;
}

static int first(smb_obj_t obj, char *name)
{
    data_t mb;
    smb_buf_t smb_buf;
    ushort vwv[2];
    int size;

    mb = VCBuffer(obj->svr->vc);
    MBSetCom(mb, SMBsearch);
    vwv[0] = 1;
    vwv[1] = 0x37;
    MBSetVWV(mb, 2, vwv);
    MBAddAscii(mb, obj->path);
    MBContinueAscii(mb, name);
    MBAddVarBlk(mb, 0, NULL);

    communicate(obj->svr);

    if(MBError(mb))
        return 0;
    MBGetVWV(mb, 1, vwv);
    if(vwv[0] == 0)
        return 0;
    if(vwv[0] != 1)
        Error("Unrequested directory entries returned");
    MBGetBuf(mb, &smb_buf);
    if(MBBufType(&smb_buf) != VARBLK)
        Error("Search didn't return a variable block");
    size = MBBufSize(&smb_buf);
    if(size < 0x2B)
        Error("Var block too small for dir info");
    memcpy(&obj->dir_info, MBBufData(&smb_buf), 0x2B);
    set_vals(obj);
    return 1;
}

static int next(smb_obj_t obj)
{
    data_t mb;
    smb_buf_t smb_buf;
    ushort vwv[2];
    int size;

    mb = VCBuffer(obj->svr->vc);
    MBSetCom(mb, SMBsearch);
    vwv[0] = 1;
    vwv[1] = 0x37;
    MBSetVWV(mb, 2, vwv);
    MBAddAscii(mb, "");
    MBAddVarBlk(mb, sizeof(search_status_t), obj->dir_info.find_buf_reserved);

    communicate(obj->svr);

    if(MBError(mb))
        return 0;
    MBGetVWV(mb, 1, vwv);
    if(vwv[0] == 0)
        return 0;
    if(vwv[0] != 1)
        Error("Unrequested directory entries returned");
    MBGetBuf(mb, &smb_buf);
    if(MBBufType(&smb_buf) != VARBLK)
        Error("Search didn't return a variable block");
    size = MBBufSize(&smb_buf);
    if(size < 0x2B)
        Error("Var block too small for dir info");
    memcpy(&obj->dir_info, MBBufData(&smb_buf), 0x2B);
    set_vals(obj);
    return 1;
}

static void flush(smb_obj_t obj)
{
    data_t mb;
    ushort vwv[8];
    int is_dir;

    is_dir = ((obj->attribute & SMB_SUBDIR) != 0);
    if(obj->dirty)
    {
        ExceptTry
        {
            mb = VCBuffer(obj->svr->vc);
            MBSetCom(mb, SMBsetatr);
            vwv[0] = (obj->attribute & ~SMB_SUBDIR);
            vwv[1] = obj->time;
            vwv[2] = (obj->time >> 16);
            vwv[3] = vwv[4] = vwv[5] = vwv[6] = vwv[7] = 0;
            MBSetVWV(mb, 8, vwv);
            MBAddAscii(mb, obj->path);
            MBContinueAscii(mb, obj->name);
            MBAddAscii(mb, "");
            communicate(obj->svr);
        }
        ExceptCatch
        {
            ExceptTry
            {
                mb = VCBuffer(obj->svr->vc);
                MBSetCom(mb, SMBsetatr);
                vwv[0] = (obj->attribute & ~SMB_SUBDIR);
                vwv[1] = vwv[2] = vwv[3] = vwv[4] = vwv[5] = vwv[6] = vwv[7] = 0;
                MBSetVWV(mb, 8, vwv);
                MBAddAscii(mb, obj->path);
                MBContinueAscii(mb, obj->name);
                MBAddAscii(mb, "");
                communicate(obj->svr);
            }
            ExceptCatch
            {
            }
            if(!is_dir)
            {
                MBSetCom(mb, SMBopen);
                vwv[0] = 0x20;
                vwv[1] = obj->attribute;
                MBSetVWV(mb, 2, vwv);
                MBAddAscii(mb, obj->path);
                MBContinueAscii(mb, obj->name);
                communicate(obj->svr);
                MBGetVWV(mb, 1, vwv);

                MBSetCom(mb, SMBclose);
                vwv[1] = obj->time;
                vwv[2] = (obj->time >> 16);
                MBSetVWV(mb, 3, vwv);
                MBAddAscii(mb, obj->path);
                MBContinueAscii(mb, obj->name);
                communicate(obj->svr);
            }
        }
        obj->dirty = 0;
    }
}


static void disk_info(smb_server_t svr, smb_disc_info_t *dinfo)
{
    data_t mb;
    ushort vwv[4];

    mb = VCBuffer(svr->vc);
    MBSetCom(mb, SMBdskattr);
    MBSetVWV(mb, 5, vwv);

    communicate(svr);

    MBGetVWV(mb, 4, vwv);
    dinfo->total = vwv[0];
    dinfo->free = vwv[3];
    dinfo->units = vwv[1] * vwv[2];
}


static protocol_info_t negotiate(vc_t vc)
{
    protocol_info_t prot_info;
    data_t mb;
    ushort vwv[17];
    nt_negprot_response_t *response;
    int i;

    mb = VCBuffer(vc);
    MBSetCom(mb, SMBnegprot);
    MBSetVWV(mb, 0, NULL);
    for(i = 0; protocols[i]; i++)
    {
        MBAddDialect(mb, protocols[i]);
    }

    send(vc);
    receive(vc);

    MBGetVWV(mb, 1, vwv);
    prot_info.dialect = -1;
    prot_info.max_raw_read = 0;
    prot_info.mode = 0;
    prot_info.session = 0;
    prot_info.capabilities = 0;
    prot_info.security_mode = 0;
    memset(prot_info.encryption_key, 0, sizeof(prot_info.encryption_key));

    /*
        0xFFFF is the server saying that none of the dialects offered are
        ones it will speak.  That is what a machine with SMB1 turned off
        answers - every current Windows, and any NAS hardened the same way -
        and it is not an error here: a dialect of -1 tells the caller to
        try SMB2 instead.
    */
    if(vwv[0] == 0xFFFF)
        return prot_info;
    if(vwv[0] >= i)
        Error("Protocol index out of range");
    prot_info.dialect = vwv[0];
    if(vwv[0] >= MN103 && vwv[0] <= LM21)
    {
        char *data;
        int size;

        MBGetVWV(mb, 13, vwv);
        VCResizeBuffer(vc, vwv[2]);
        prot_info.mode = (vwv[1] & 3);
        prot_info.session = (vwv[6] | (vwv[7] << 16));
        if(vwv[5] & 1) prot_info.max_raw_read = 0x10000;

        data  = MBGetRawBuf(mb, &size);
        if(vwv[11] == 8)
        {
            if(size < vwv[11])
                Error("Encryption key smaller than expected");
            memcpy(prot_info.encryption_key, data, vwv[11]);
        }
        else
        {
            prot_info.mode &= ~2;
        }
    }
    if(vwv[0] > LM21)
    {
        char *data;
        int size;

        MBGetVWV(mb, 17, vwv);
        response = (nt_negprot_response_t *) vwv;
        VCResizeBuffer(vc, Word32(response->max_buffer_size));
        prot_info.mode = (response->security_mode & 3);
        prot_info.security_mode = (int) (unsigned char) response->security_mode;
        prot_info.session = Word32(response->session_key);
        prot_info.capabilities = Word32(response->capabilities);
        if(Word32(response->capabilities) & 1)
            prot_info.max_raw_read = Word32(response->max_raw_size);

        data  = MBGetRawBuf(mb, &size);
        if(response->encryption_key_length == 8)
        {
            if(size < response->encryption_key_length)
                Error("Encryption key smaller than expected");
            memcpy(prot_info.encryption_key, data, response->encryption_key_length);
        }
        else
        {
            prot_info.mode &= ~2;
        }
    }
    if(vwv[0] == MN103)
        prot_info.max_raw_read = 0;

    prot_info.max_raw_read = MIN(prot_info.max_raw_read, 0xffff) & ~3;

    // printf("Max raw read size = 0x%04x\n", prot_info.max_raw_read);
    return prot_info;
}

static void tree_connect(smb_server_t svr, char *path, char *passwd, char *device)
{
    data_t mb;
    ushort vwv[2];

#ifdef DEBUG
    printf("Tree connect (passwd = %s)\n", passwd);
#endif
    mb = VCBuffer(svr->vc);
    MBSetCom(mb, SMBtcon);
    MBSetVWV(mb, 0, NULL);
    MBAddAscii(mb, path);
    MBAddAscii(mb, passwd);
    MBAddAscii(mb, device);
    communicate(svr);
    MBGetVWV(mb, 2, vwv);
    MBSetTid(mb, vwv[1]);
    if(svr->dialect <= MN103)
        VCResizeBuffer(svr->vc, vwv[0]);
}

static int vc_num(void)
{
    static int vc = 0;

    if(++vc > 0xFFF) vc = 1;
    return vc;
}


static char lmhash_magic[8] = {'K', 'G', 'S', '!', '@', '#', '$', '%'};

/* Password null terminated, result is 16 bytes */
static void lm_hash(char *passwd, char *result)
{
    char upper_passwd[14];
    int i;
    des_key_t key;

    /* Truncate password to 14 characters, if shorter then pad with zeros */
    strncpy(upper_passwd, passwd, 14);

    for (i = 0; i < 14; i++)
        upper_passwd[i] = toupper(upper_passwd[i]);

    key = DesKey(upper_passwd);
    DesEncrypt(key, lmhash_magic, result);
    DesKeyDestruct(key);
    key = DesKey(upper_passwd + 7);
    DesEncrypt(key, lmhash_magic, result + 8);
    DesKeyDestruct(key);
}

/* Password null terminated, result is 16 bytes */
static void ntlm_hash(char *passwd, char *result)
{
    char *unicode_passwd;
    int i;
    int len = strlen(passwd);

    unicode_passwd = Malloc(len * 2);
    memset(unicode_passwd, 0, len * 2);

    /* Simplistically convert from ASCII to UCS-2LE */
    for (i = 0; i < len; i++)
        unicode_passwd[2*i] = passwd[i];

    md4_get_digest(unicode_passwd, len * 2, (unsigned char *)result);
    Free(unicode_passwd);
}

static void encrypt_password(char *hash, char *challenge, char *response)
{
    char P21[21];
    int i;

    memcpy(P21, hash, 16);
    memset(P21 + 16, 0, 5);

    for (i = 0; i < 3; i++)
    {
        des_key_t key;
        key = DesKey(P21 + (7 * i));
        DesEncrypt(key, challenge, response + (8 * i));
        DesKeyDestruct(key);
    }
}


/* Which authentication scheme one session setup attempt should use */
#define AUTH_LM     (0)     /* pre-NT dialect: LM response only */
#define AUTH_NTLMV1 (1)
#define AUTH_NTLMV2 (2)

/*
    A client challenge for NTLMv2.  This is not a cryptographic random
    source, which RISC OS does not offer a filing system module, but it
    does have to differ from one logon to the next: a fixed value would
    give away the whole point of having the client contribute at all.
*/
static void client_nonce(unsigned char nonce[NTLM_CHAL_LEN])
{
    static unsigned int counter = 0;
    struct { unsigned int t, c, n, a; } seed;
    unsigned char digest[MD5_RESULTLEN];

    seed.t = (unsigned int) time(NULL);
    seed.c = (unsigned int) clock();
    seed.n = ++counter;
    seed.a = (unsigned int) (int) &seed;
    md5_get_digest(&seed, sizeof(seed), digest);
    memcpy(nonce, digest, NTLM_CHAL_LEN);
}

/*
    The domain to authenticate in.  Whatever this is, the same text has to
    go into NTOWFv2 and onto the wire, or the server derives a different
    key and refuses the logon.  Empty suits a standalone server or a NAS,
    which is what LanMan98 mostly talks to.
*/
static void auth_domain(char *buf, int len)
{
    char *var;

    buf[0] = 0;
    var = getenv("LanMan98$Domain");
    if(var)
    {
        strncpy(buf, var, len - 1);
        buf[len - 1] = 0;
    }
}

static void session_setup_try(smb_server_t svr, char *user, char *passwd, int session, char *challenge, int scheme)
{
    data_t mb;
    ushort vwv[13];
    char response[24];
    unsigned char lm_resp[NTLM_LMV2_LEN];
    unsigned char nt_resp[NTLM_NTV2_MIN + 64];
    char domain[64];
    int lm_len, nt_len;
    char *data;
    int size;

#ifdef DEBUG
    printf("Session setup (user = %s, passwd = %s)\n", user, passwd);
#endif
    domain[0] = 0;
    lm_len = 0;
    nt_len = 0;
    if(challenge && scheme != AUTH_LM)
    {
        char nt_hash[NTLM_HASH_LEN];

        ntlm_hash(passwd, nt_hash);
        if(scheme == AUTH_NTLMV2)
        {
            unsigned char key[NTLM_HASH_LEN];
            unsigned char nonce[NTLM_CHAL_LEN];
            unsigned int ft_low, ft_high;

            auth_domain(domain, sizeof(domain));
            client_nonce(nonce);
            DateSecondsToFileTime((unsigned int) time(NULL),
                                  &ft_low, &ft_high);
            NTLMv2Key(user, domain, (unsigned char *) nt_hash, key);
            LMv2Response(key, challenge, nonce, lm_resp);
            lm_len = NTLM_LMV2_LEN;
            nt_len = NTLMv2Response(key, challenge, nonce, ft_low, ft_high,
                                    NULL, 0, nt_resp, sizeof(nt_resp));
            memset(key, 0, sizeof(key));
            if(nt_len == 0)
                Error("Could not build the NTLMv2 response");
        }
        else
        {
            char lmhash[16];

            /* NTLMv1: the same DES transform over both hashes.  Servers
               with lanman auth disabled reject the LM half outright, which
               is why this is now only reached as a fallback. */
            lm_hash(passwd, lmhash);
            encrypt_password(lmhash, challenge, (char *) lm_resp);
            lm_len = sizeof(response);
            encrypt_password(nt_hash, challenge, (char *) nt_resp);
            nt_len = sizeof(response);
        }
        memset(nt_hash, 0, sizeof(nt_hash));
    }

    mb = VCBuffer(svr->vc);
    MBSetCom(mb, SMBsesssetup);
    if(svr->dialect < NT)
    {
        vwv[0] = 0xFF;
        vwv[1] = 0;
        vwv[2] = mb.size;
        vwv[3] = 2;
        vwv[4] = 0;
        vwv[5] = 0;
        vwv[6] = 0;
        if (challenge)
            vwv[7] = sizeof(response);
        else
            vwv[7] = strlen(passwd)+1;
        vwv[8] = 0;
        vwv[9] = 0;
        MBSetVWV(mb, 10, vwv);
        if (challenge)
        {
            char lmhash[16];

            lm_hash(passwd, lmhash);
            encrypt_password(lmhash, challenge, response);
            MBAddRaw(mb, sizeof(response), response);
        }
        else
        {
            MBAddRaw(mb, strlen(passwd)+1, passwd);
        }
        MBAddRaw(mb, strlen(user)+1, user);
        MBAddRaw(mb, 1, "");
        MBAddRaw(mb, strlen("RISC OS")+1, "RISC OS");
        MBAddRaw(mb, strlen("CIFS")+1, "CIFS");
    }
    else
    {
        vwv[0] = 0xFF;
        vwv[1] = 0;
        vwv[2] = mb.size;
        vwv[3] = 2;
        vwv[4] = vc_num();
        vwv[5] = session;
        vwv[6] = (session >> 16);
        if (challenge)
        {
            vwv[7] = sizeof(response);
            vwv[8] = sizeof(response);
        }
        else
        {
            vwv[7] = strlen(passwd);
            vwv[8] = 0;
        }
        vwv[9] = 0;
        vwv[10] = 0;
        /*
            Client capabilities.  Asking for 32 bit status codes takes both
            this and the flags2 bit: a server told only through flags2 goes
            on answering in the DOS scheme, because what it records for the
            session is what the client declared here.
        */
        {
            unsigned int caps;

            caps = svr->client_caps;
            vwv[11] = (ushort) (caps & 0xFFFF);
            vwv[12] = (ushort) (caps >> 16);
        }
        if (challenge)
        {
            vwv[7] = lm_len;
            vwv[8] = nt_len;
        }
        MBSetVWV(mb, 13, vwv);
        if (challenge)
        {
            MBAddRaw(mb, lm_len, lm_resp);
            MBAddRaw(mb, nt_len, nt_resp);
        }
        else
        {
            MBAddRaw(mb, strlen(passwd), passwd);
        }
        MBAddRaw(mb, strlen(user)+1, user);
        MBAddRaw(mb, strlen(domain)+1, domain);
        MBAddRaw(mb, strlen("RISC OS")+1, "RISC OS");
        MBAddRaw(mb, strlen("CIFS")+1, "CIFS");
    }

    communicate(svr);
#ifdef PRINT_UID
    printf("UID = %d\n", MBGetUid(mb));
#endif
    data = MBGetRawBuf(mb, &size);

#define BLACKLIST(x) do { \
  int len = size; \
  if (sizeof(x) - 1 < len) \
      len = sizeof(x) - 1; \
  if (memcmp(data, x, len) == 0) \
      svr->max_raw_read = 0; \
} while (0)

    /* Although Vista claims to support READ_RAW in the capabilities,
       it does not appear to respond at all to such requests */
    BLACKLIST("Windows Vista");

    BLACKLIST("Unix\0Samba 3.0.20");

    if (getenv("LanMan98$ReadRaw") == NULL)
    {
        svr->max_raw_read = 0;
    }

    /* This is a heuristic to detect Freecom which appears to be
       broken in the same way as NAS-BASICxx firmware. */
    if ((size == 12) && (memcmp(data, "\0R\0\0\0R\0\0\0R\0\0", size) == 0))
    {
        svr->nas_basic_firmware = 1;
    }
}

/*
    Authenticate, preferring NTLMv2.

    NTLMv1 is refused by default by Samba, by Windows above
    LmCompatibilityLevel 2 and by macOS, so v2 has to be tried first.  The
    NAS boxes LanMan98 has always worked with predate v2 entirely, though,
    so a refusal falls back once rather than simply failing.

    Only an authentication refusal is worth retrying: a dropped line or a
    malformed reply would fail again, and the second, less informative
    error would be the one the user saw.  The two error numbers are the
    same whether the server answered with an NT status or the older class
    and code, which is what makes this check safe to write once.
*/
/*
    Session setup the way every current server wants it: the NTLMSSP
    exchange, carried in the security blob of a twelve word SESSION_SETUP.

    The older scheme this replaces put an NTLMv2 response straight into the
    request.  Samba has refused that since 4.5 - "raw NTLMv2 auth" defaults
    off - and answers STATUS_INVALID_PARAMETER, which is what a Synology,
    a QNAP or any recent Linux box will say to the plain scheme.  The
    machinery here is the same NTLMSSP that c.smb2 uses; only the envelope
    differs, so both protocols authenticate identically.
*/
#define NTLMSSP_BLOB_MAX (1536)

static void session_setup_ntlmssp(smb_server_t svr, char *user, char *passwd,
                                  int session, int sec_mode)
{
    data_t mb;
    ushort vwv[12];
    ushort rvwv[4];
    unsigned char *blob;
    unsigned char *chal_copy;
    unsigned char nt_hash[NTLM_HASH_LEN];
    unsigned char session_key[NTLM_HASH_LEN];
    unsigned char challenge[NTLM_CHAL_LEN];
    unsigned char nonce[NTLM_CHAL_LEN];
    const unsigned char *target;
    char domain[64];
    unsigned int status, ft_low, ft_high, caps;
    int blob_len, target_len, size, n, i;
    char *data;

    auth_domain(domain, sizeof(domain));

    /* --- first leg: say what this client can do --- */
    mb = VCBuffer(svr->vc);
    blob = Malloc(NTLMSSP_BLOB_MAX);
    ExceptTry
    {
        blob_len = NTLMSSPNegotiate(blob, NTLMSSP_BLOB_MAX);
        if(blob_len == 0)
            Error("No room for the NTLMSSP negotiate message");

        MBSetCom(mb, SMBsesssetup);
        /* No session exists yet, and the header is still carrying the
           identifier the negotiate left in it.  Starting an exchange
           under one the server never issued is answered ERRbaduid. */
        MBSetUid(mb, 0);
        vwv[0] = 0xFF;
        vwv[1] = 0;
        vwv[2] = (ushort) ((mb.size > 0xFFFF) ? 0xFFFF : mb.size);
        vwv[3] = 2;
        vwv[4] = vc_num();
        vwv[5] = session;
        vwv[6] = (session >> 16);
        vwv[7] = (ushort) blob_len;     /* SecurityBlobLength */
        vwv[8] = 0;                     /* Reserved */
        vwv[9] = 0;
        caps = (unsigned int) svr->client_caps;
        vwv[10] = (ushort) (caps & 0xFFFF);
        vwv[11] = (ushort) (caps >> 16);
        MBSetVWV(mb, 12, vwv);
        MBAddRaw(mb, blob_len, blob);
        MBAddRaw(mb, strlen("RISC OS") + 1, "RISC OS");
        MBAddRaw(mb, strlen("CIFS") + 1, "CIFS");
    }
    ExceptCatch
    {
        Free(blob);
        ExceptRethrow();
    }

    /*
        The reply is an error as far as the layer below is concerned, so it
        is collected without the usual mapping and read here instead.
    */
    svr->send(svr->vc);
    VCReceive(svr->vc);
    mb = VCBuffer(svr->vc);
    if(!MBIsNTStatus(mb))
    {
        Free(blob);
        Error("Server answered extended security without an NT status");
    }
    status = MBGetNTStatus(mb);
    if(status != NT_MORE_PROCESSING)
    {
        Free(blob);
        if(status == 0)
            Error("Server accepted the session without a challenge");
        SMBErrorStatus(status);
    }

    MBGetVWV(mb, 4, rvwv);
    data = MBGetRawBuf(mb, &size);
    n = rvwv[3];                        /* SecurityBlobLength */
    if((n <= 0) || (n > size))
    {
        Free(blob);
        Error("NTLMSSP challenge is not where the reply says it is");
    }

    /*
        A server that wraps its answer in SPNEGO puts the same message a
        little way in, so it is found rather than assumed to be first.
    */
    for(i = 0; i + 8 <= n; i++)
    {
        if(memcmp(data + i, "NTLMSSP", 8) == 0)
            break;
    }
    if(i + 8 > n)
    {
        Free(blob);
        Error("Server did not answer with an NTLMSSP challenge");
    }

    /* The challenge has to outlive the buffer it arrived in: the next
       request is built over the top of it. */
    chal_copy = Malloc(n - i);
    memcpy(chal_copy, data + i, n - i);

    ExceptTry
    {
        if(!NTLMSSPParseChallenge(chal_copy, n - i, challenge, &target,
                                  &target_len))
            Error("Server did not answer with an NTLMSSP challenge");

        ntlm_hash(passwd, (char *) nt_hash);
        client_nonce(nonce);
        DateSecondsToFileTime((unsigned int) time(NULL), &ft_low, &ft_high);

        blob_len = NTLMSSPAuthenticate(user, domain, "RISCOS", nt_hash,
                                       challenge, nonce, ft_low, ft_high,
                                       target, target_len, session_key,
                                       blob, NTLMSSP_BLOB_MAX);
        if(blob_len == 0)
            Error("No room for the NTLMSSP authenticate message");

        /* --- second leg: the answer.  The user identifier the server set
           on the first leg is still in the header and has to stay. --- */
        mb = VCBuffer(svr->vc);
        MBSetCom(mb, SMBsesssetup);
        vwv[7] = (ushort) blob_len;
        MBSetVWV(mb, 12, vwv);
        MBAddRaw(mb, blob_len, blob);
        MBAddRaw(mb, strlen("RISC OS") + 1, "RISC OS");
        MBAddRaw(mb, strlen("CIFS") + 1, "CIFS");

        svr->send(svr->vc);
        VCReceive(svr->vc);
        mb = VCBuffer(svr->vc);
        status = MBIsNTStatus(mb) ? MBGetNTStatus(mb) : 0;
        if(status != 0)
            SMBErrorStatus(status);
    }
    ExceptCatch
    {
        memset(nt_hash, 0, sizeof(nt_hash));
        memset(session_key, 0, sizeof(session_key));
        Free(chal_copy);
        Free(blob);
        ExceptRethrow();
    }
    memset(nt_hash, 0, sizeof(nt_hash));
    Free(chal_copy);
    Free(blob);

    /*
        Signing, if the server insists on it or it has been asked for.

        The reply that just arrived is the first message the server signed,
        and it carries sequence number one; checking it proves the server
        derived the same key.  The request it answered was number zero, so
        the next one out is number two, and the count goes up in twos from
        there.  Neither leg of the exchange itself is signed - the key they
        establish is the thing that would sign them.
    */
    if((sec_mode & SEC_SIGN_REQUIRED) || (getenv("LanMan98$Sign") != NULL))
    {
        if(!VCCheckSignature(svr->vc, session_key, sizeof(session_key), 1))
        {
            memset(session_key, 0, sizeof(session_key));
            Error("Server's reply failed its signature check");
        }
        VCSignFrom(svr->vc, session_key, sizeof(session_key), 2);
    }
    memset(session_key, 0, sizeof(session_key));

    /* READ_RAW is off unless it was asked for; the vendor checks the plain
       scheme does here read the native operating system string, which sits
       behind the security blob in this reply and is not worth digging out
       for a path that is disabled by default. */
    if(getenv("LanMan98$ReadRaw") == NULL)
        svr->max_raw_read = 0;
}

static void session_setup(smb_server_t svr, char *user, char *passwd, int session, char *challenge, int sec_mode)
{
    volatile int done;

    if(svr->dialect < NT)
    {
        session_setup_try(svr, user, passwd, session, challenge, AUTH_LM);
        return;
    }

    /* Extended security when the server offered it: the plain schemes
       below are refused outright by anything current. */
    if(svr->client_caps & CAP_EXTENDED_SECURITY)
    {
        session_setup_ntlmssp(svr, user, passwd, session, sec_mode);
        return;
    }

    done = 0;
    if(challenge && (getenv("LanMan98$NTLMv1") == NULL))
    {
        ExceptTry
        {
            session_setup_try(svr, user, passwd, session, challenge,
                              AUTH_NTLMV2);
            done = 1;
        }
        ExceptCatch
        {
            int errnum;

            errnum = ExceptCaught()->errnum;
            /* 0x20002 bad password or logon failure, 0x10005 access denied */
            if(errnum != 0x20002 && errnum != 0x10005)
                ExceptRethrow();
        }
    }
    if(!done)
        session_setup_try(svr, user, passwd, session, challenge, AUTH_NTLMV1);
}


static void tree_connect_andx(smb_server_t svr, char *path, char *passwd, char *device, char *challenge)
{
    data_t mb;
    ushort vwv[4];
    char response[24];

#ifdef DEBUG
    printf("Tree connect andx (passwd = %s)\n", passwd);
#endif
    mb = VCBuffer(svr->vc);
    MBSetCom(mb, SMBtconandx);
    vwv[0] = 0xff;
    vwv[1] = 0;
    vwv[2] = 0;
    vwv[3] = challenge ? sizeof(response) : strlen(passwd);
    MBSetVWV(mb, 4, vwv);
    if (challenge)
    {
        char lmhash[16];

        lm_hash(passwd, lmhash);
        encrypt_password(lmhash, challenge, response);
        MBAddRaw(mb, sizeof(response), response);
    }
    else
    {
        MBAddRaw(mb, strlen(passwd), passwd);
    }
    MBAddRaw(mb, strlen(path) + 1, path);
    MBAddRaw(mb, strlen(device) + 1, device);
    communicate(svr);
}

/*
    Try SMB2 before SMB1.

    Only when asked: the negotiation, the authentication and the share
    connection are implemented, but the file operations above them are not,
    so a mount that took this route would connect and then be unable to do
    anything.  Until those exist this stays behind LanMan98$SMB2, where it
    can be exercised without changing what anyone else gets.

    A server that does not answer SMB2 leaves the circuit in an unknown
    state, so the fallback reopens it rather than reusing it.
*/
/*
    Whether the older protocol is allowed at all.

    LanMan98$SMB2 says to prefer SMB2, but a server that will not speak it
    still gets talked to the old way.  That is usually what is wanted - a
    connection that works beats one that does not - but it means a server
    quietly falling back cannot be told from one that never tried.
    LanMan98$SMB2Only refuses the fallback, so a connection either uses
    SMB2 or says why it could not.
*/
/*
    Pinning a protocol level, for finding out where a fault lives.

    LanMan98$SMB1, $SMB2 and $SMB3 each say "use this and nothing else".
    A server that will not speak the one asked for is reported rather than
    quietly talked to some other way, which is the whole point: a fault
    that appears at one level and not another cannot be placed while the
    level in use is a guess.

    $SMB2 and $SMB3 also narrow which dialects are offered - see c.smb2.
*/
static int forced_smb1(void)
{
    return getenv("LanMan98$SMB1") != NULL;
}

static int forced_smb2(void)
{
    return (getenv("LanMan98$SMB2") != NULL) ||
           (getenv("LanMan98$SMB3") != NULL);
}

static int smb2_only(void)
{
    return (getenv("LanMan98$SMB2Only") != NULL) || forced_smb2();
}

/*
    Whether to try SMB2 before the older protocol.

    It is tried first now.  It is what every current server offers, several
    of them offer nothing else, and everything the older protocol could do -
    printing was the last of it - now works over it.  Trying the old one
    first meant the common case took the worse of the two.

    LanMan98$NoSMB2 goes back to the old order for a server that handles
    SMB2 badly.  LanMan98$SMB2 is no longer needed and is ignored; setting
    it does no harm, so anything that already does keeps working.
*/
static int smb2_wanted(void)
{
    if(forced_smb1())
        return 0;
    if(smb2_only())
        return 1;
    return getenv("LanMan98$NoSMB2") == NULL;
}

static int try_smb2(smb_server_t svr, char *addr, tcp_port_t port,
                    char *netbios_name, char *share, char *user, char *passwd,
                    int only_way_in)
{
    char domain[64];

    if(!only_way_in && !smb2_wanted())
        return 0;

    auth_domain(domain, sizeof(domain));
    svr->smb2 = SMB2Connect(svr->vc, share, user, domain, passwd);
    if(svr->smb2 == NULL)
    {
        /* The circuit had an SMB2 negotiate on it and cannot be reused,
           whether or not anything else is going to be tried on it. */
        VCDestruct(svr->vc);
        svr->vc = VC(addr, port, netbios_name);
        MBInit(VCBuffer(svr->vc));
        if(smb2_only())
            Error("This server does not answer SMB2, and LanMan98$SMB2Only "
                  "says not to use the older protocol instead");
        return 0;
    }
    return 1;
}

static void smb_connect1(smb_server_t svr, char *addr, tcp_port_t port, char *netbios_name, char *share, char *user, char *passwd, char *device)
{
    protocol_info_t prot_info;

    svr->vc = VC(addr, port, netbios_name);
    svr->dialect = 0;
    svr->max_raw_read = 0;
    svr->ro2smb = xro2smb;
    svr->smb2ro = xsmb2ro;
    svr->send = send;
    svr->receive = receive;
    svr->name_map = name_map;
    svr->construct = construct;
    svr->dup = dup;
    svr->destruct = destruct;
    svr->form = first;
    svr->first = first;
    svr->next = next;
    svr->flush = flush;
    svr->disk_info = disk_info;
    svr->nas_basic_firmware = 0;
    svr->smb2 = NULL;
    MBInit(VCBuffer(svr->vc));

    VCLog("connecting to %s share %s", addr ? addr : "?", share ? share : "?");
    if(try_smb2(svr, addr, port, netbios_name, share, user, passwd, 0))
    {
        VCLog("     using SMB2");
        svr->dialect = NT;      /* so the layer above asks for long names */
        SMB2Upgrade(svr);
        return;
    }

    /*
        Both of these have to be asked for in the negotiate itself.

        A server offers extended security only to a client that set the
        flag there, and without it the only schemes left are ones a current
        server refuses.  The NT status bit matters for the same reason: the
        scheme a server answers a session setup in is the one the negotiate
        established, so asking only afterwards leaves the interim reply of
        an NTLMSSP exchange - the one carrying the challenge - in the DOS
        scheme, which has no code that stands for "more processing".

        LanMan98$NoExtSec and LanMan98$NoNTStatus each go back to the old
        way for a server that mishandles one of them.
    */
    {
        int ask;

        ask = 0;
        if(getenv("LanMan98$NoNTStatus") == NULL)
            ask |= SMB_FLAGS2_NT_STATUS;
        if(getenv("LanMan98$NoExtSec") == NULL)
            ask |= SMB_FLAGS2_EXT_SEC;
        /*
            A server that only offers signing turns it on for a client that
            asks in the negotiate, and not otherwise, so asking has to
            happen here.  A server that insists on signing needs no asking.
            It is left off by default because signing every message costs
            an MD5 over all of it, which on a slow machine is a real part
            of the transfer time and buys nothing on a trusted network.
        */
        if(getenv("LanMan98$Sign") != NULL)
            ask |= SMB_FLAGS2_SIGNATURE;
        VCSetFlags2(svr->vc, ask);
    }

    prot_info = negotiate(svr->vc);

    /*
        The server would not speak SMB1 at all.  SMB2 is then the only way
        in, so it is tried whether or not it was asked for: there is nothing
        left to fall back to, and refusing to try would mean answering a
        perfectly reachable server with "protocol index out of range".

        The circuit has had an SMB1 negotiate on it and cannot be reused,
        so it is reopened first.
    */
    if(prot_info.dialect < 0)
    {
        if(forced_smb1())
            Error("This server has SMB1 turned off, and LanMan98$SMB1 says "
                  "to use nothing else");
        VCDestruct(svr->vc);
        svr->vc = VC(addr, port, netbios_name);
        MBInit(VCBuffer(svr->vc));
        if(try_smb2(svr, addr, port, netbios_name, share, user, passwd, 1))
        {
            svr->dialect = NT;
            SMB2Upgrade(svr);
            return;
        }
        Error("This server has SMB1 turned off and did not answer SMB2 "
              "either, so there is no protocol left to talk to it with");
    }

    if(smb2_only())
    {
        /* Reached only when the server answered an SMB1 negotiate, which
           means SMB2 was tried first and declined - already reported. */
        Error("This server answered only the older protocol, and "
              "LanMan98$SMB2Only says not to use it");
    }

    VCLog("     using SMB1, dialect index %d", prot_info.dialect);
    svr->dialect = prot_info.dialect;
    svr->max_raw_read = prot_info.max_raw_read;
    svr->client_caps = 0;
    svr->use_andx = 0;
    svr->max_xfer = 0;

    /* Every request from here on asks for 32 bit NT status codes.  The DOS
       class and code pair a server falls back on cannot tell an access
       denial from a bad password from a sharing violation, which makes a
       failed mount very hard to account for.  LanMan98$NoNTStatus exists
       for the server that negotiates the capability and then mishandles it. */
    {
        int flags2;

        flags2 = 0;
        if((svr->dialect >= NT) &&
           (prot_info.capabilities & CAP_STATUS32) &&
           (getenv("LanMan98$NoNTStatus") == NULL))
        {
            flags2 |= SMB_FLAGS2_NT_STATUS;
            svr->client_caps |= CAP_STATUS32;
        }
        /* Extended security needs the NT status codes as well: the first
           leg of the exchange is reported as one and there is no DOS class
           and code that stands for it. */
        if((svr->dialect >= NT) &&
           (prot_info.capabilities & CAP_EXTENDED_SECURITY) &&
           (flags2 & SMB_FLAGS2_NT_STATUS))
        {
            flags2 |= SMB_FLAGS2_EXT_SEC;
            svr->client_caps |= CAP_EXTENDED_SECURITY;
        }
        if((prot_info.security_mode & SEC_SIGN_REQUIRED) ||
           (getenv("LanMan98$Sign") != NULL))
            flags2 |= SMB_FLAGS2_SIGNATURE;
        VCSetFlags2(svr->vc, flags2);
    }

    /*
        READ_ANDX and WRITE_ANDX replace the core read and write pair.  The
        old commands can only carry as much as the buffer agreed at
        negotiation, about sixteen kilobytes, so a file moves in that many
        round trips; the large capabilities lift that to sixty.  Both are
        claimed only if the server offered them, and only together with the
        buffer that has to hold the result.
    */
    if((svr->dialect >= NT) && (getenv("LanMan98$NoReadX") == NULL))
    {
        svr->use_andx = 1;
        if(prot_info.capabilities & CAP_LARGE_READX)
            svr->client_caps |= CAP_LARGE_READX;
        if(prot_info.capabilities & CAP_LARGE_WRITEX)
            svr->client_caps |= CAP_LARGE_WRITEX;
    }
    if(svr->dialect >= LM2)
        svr = LM2Upgrade(svr, (prot_info.mode & 1));
    if(svr->dialect >= LM1)
    {
        session_setup(svr, user, passwd, prot_info.session, (prot_info.mode & 2) ? prot_info.encryption_key : NULL, prot_info.security_mode);
        tree_connect_andx(svr, share, ((prot_info.mode & 1) ? "" : passwd), device, ((prot_info.mode & 3) == 2) ? prot_info.encryption_key : NULL);
    }
    else
    {
        tree_connect(svr, share, ((prot_info.mode & 1) ? "" : passwd), device);
    }

    if(svr->use_andx)
    {
        data_t mb;

        /* Grow the buffer to hold a large transfer, and settle for the
           negotiated size if the heap will not stretch to it. */
        if(svr->client_caps & (CAP_LARGE_READX | CAP_LARGE_WRITEX))
        {
            ExceptTry
            {
                VCResizeBuffer(svr->vc, LARGE_XFER + 1024);
            }
            ExceptCatch
            {
            }
        }
        mb = VCBuffer(svr->vc);
        svr->max_xfer = mb.size - MBDataOffset(14) - 64;
        if(svr->max_xfer > LARGE_XFER)
            svr->max_xfer = LARGE_XFER;
        svr->max_xfer &= ~3;
        if(svr->max_xfer <= 0)
            svr->use_andx = 0;
    }

    /* This is a heuristic to detect Landisk NAS-BASICxx firmware which is
       rather broken in several respects. */
    if ((svr->dialect == NT) &&
        (prot_info.mode == 2) &&
        (memcmp(prot_info.encryption_key, "\0\0\0\0\0\0\0\0", 8) == 0))
    {
        svr->nas_basic_firmware = 1;
    }
}

static smb_server_t smb_connect(char *addr, tcp_port_t port, char *netbios_name, char *share, char *user, char *passwd, char *device)
{
    smb_server_t svr;

    // DiagInit();
    svr = Malloc(sizeof(*svr));
    svr->vc = NULL;
    ExceptTry
    {
        smb_connect1(svr, addr, port, netbios_name, share, user, passwd, device);
    }
    ExceptCatch
    {
        SMBDropServer(svr);
        ExceptRethrow();
    }
    return svr;
}

void SMBReconnect(smb_server_t svr, char *addr, tcp_port_t port, char *netbios_name, char *share, char *user, char *passwd)
{
    if(svr->vc)
    {
        VCDestruct(svr->vc);
        svr->vc = NULL;
    }
    smb_connect1(svr, addr, port, netbios_name, share, user, passwd, disc_device);
}

smb_server_t SMBConnect(char *addr, tcp_port_t port, char *netbios_name, char *share, char *user, char *passwd)
{
    return smb_connect(addr, port, netbios_name, share, user, passwd, disc_device);
}

smb_server_t SMBConnectPrinter(char *addr, tcp_port_t port, char *netbios_name, char *share, char *user, char *passwd)
{
    return smb_connect(addr, port, netbios_name, share, user, passwd, printer_device);
}

tcp_port_t SMBPort(smb_server_t svr)
{
    return svr ? VCPort(svr->vc) : SMB_PORT_AUTO;
}

smb_server_t SMBConnectRAP(char *addr, tcp_port_t port, char *netbios_name, char *user, char *passwd)
{
    static char share[64];

    sprintf(share, "\\\\%s\\%s", netbios_name, "IPC$");
    return smb_connect(addr, port, netbios_name, share, user, passwd, rap_device);
}

void SMBDiskInfo(smb_server_t svr, smb_disc_info_t *dinfo)
{
    svr->disk_info(svr, dinfo);
}


void *SMBServerSMB2(smb_server_t svr)
{
    return svr ? svr->smb2 : NULL;
}

void SMBDropServer(smb_server_t svr)
{
    if(svr)
    {
        if(svr->smb2)
        {
            SMB2Disconnect((smb2_conn_t) svr->smb2);
            svr->smb2 = NULL;
        }
        if(svr->vc) VCDestruct(svr->vc);
        Free(svr);
    }
}


char *SMBNameMap(char *name, smb_server_t svr)
{
    return svr->name_map(name);
}

void SMBKeepAlive(smb_server_t svr)
{
    smb_obj_t obj;

    if(svr->smb2)
    {
        SMB2Echo((smb2_conn_t) svr->smb2);
        return;
    }
    obj = svr->construct(svr);
    obj->path = strdup("\\");
    ExceptTry
    {
        svr->first(obj, "Dummy");
    }
    ExceptCatch
    {
        SMBObjDestruct(obj);
        ExceptRethrow();
    }
    SMBObjDestruct(obj);
}

void SMBFlushLine(smb_server_t svr)
{
    VCFlush(svr->vc);
}

/*
    Make a file or a directory over SMB2 and fill the object in from it.
    The two differ only in which request is used, so they share this.
*/
static smb_obj_t smb2_make(smb_obj_t obj, char *name, int directory)
{
    char *path;

    obj = SMBObjDup(obj);
    path = NULL;
    ExceptTry
    {
        extend_path(obj);
        if(obj->name) Free(obj->name);
        obj->name = NULL;
        obj->name = obj->svr->ro2smb(name);
        path = SMB2Path(obj->path, obj->name);
        if(directory)
            SMB2MakeDir((smb2_conn_t) obj->svr->smb2, path);
        else
            SMB2MakeFile((smb2_conn_t) obj->svr->smb2, path);
        Free(path);
        path = NULL;
        if(!obj->svr->form(obj, obj->name))
            Error(directory ? "Directory not found" : "File not found");
        obj->dirty = 0;
    }
    ExceptCatch
    {
        if(path) Free(path);
        SMBObjDestruct(obj);
        ExceptRethrow();
    }
    return obj;
}

smb_obj_t SMBCreateFile(smb_obj_t obj, char *name)
{
    data_t mb;
    ushort vwv[3];

    if(obj->svr->smb2)
        return smb2_make(obj, name, 0);

    obj = SMBObjDup(obj);
    ExceptTry
    {
        extend_path(obj);
        if(obj->name) Free(obj->name);
        obj->name = NULL;
        obj->name = obj->svr->ro2smb(name);
        mb = VCBuffer(obj->svr->vc);
        MBSetCom(mb, SMBcreate);
        vwv[0] = 0;
        vwv[1] = 0;
        vwv[2] = 0;
        MBSetVWV(mb, 3, vwv);
        MBAddAscii(mb, obj->path);
        MBContinueAscii(mb, obj->name);
        // DiagPrint("Create file: %s%s\n", obj->path, obj->name);

        communicate(obj->svr);

        MBGetVWV(mb, 1, vwv);
        MBSetCom(mb, SMBclose);
        vwv[1] = 0;
        vwv[2] = 0;
        MBSetVWV(mb, 3, vwv);

        communicate(obj->svr);

        if(!obj->svr->form(obj, obj->name))
            Error("File not found");
        obj->dirty = 0;
    }
    ExceptCatch
    {
        SMBObjDestruct(obj);
        ExceptRethrow();
    }
    return obj;
}


smb_obj_t SMBCreateDirectory(smb_obj_t obj, char *name)
{
    data_t mb;

    if(obj->svr->smb2)
        return smb2_make(obj, name, 1);

    obj = SMBObjDup(obj);
    ExceptTry
    {
        extend_path(obj);
        if(obj->name) Free(obj->name);
        obj->name = NULL;
        obj->name = obj->svr->ro2smb(name);
        mb = VCBuffer(obj->svr->vc);
        MBSetCom(mb, SMBmkdir);
        MBSetVWV(mb, 0, NULL);
        MBAddAscii(mb, obj->path);
        MBContinueAscii(mb, obj->name);

        communicate(obj->svr);

        if(!obj->svr->form(obj, obj->name))
            Error("Directory not found");
        obj->dirty = 0;
    }
    ExceptCatch
    {
        SMBObjDestruct(obj);
        ExceptRethrow();
    }
    return obj;
}


void SMBRename(smb_obj_t obj, char *name)
{
    data_t mb;
    ushort vwv[1];

    name = obj->svr->ro2smb(name);

    if(obj->svr->smb2)
    {
        char *from, *to;

        from = SMB2Path(obj->path, obj->name);
        to = NULL;
        ExceptTry
        {
            to = SMB2Path(obj->path, name);
            SMB2Rename((smb2_conn_t) obj->svr->smb2, from, to,
                       (obj->attribute & SMB_SUBDIR) ? 1 : 0);
        }
        ExceptCatch
        {
            Free(from);
            if(to) Free(to);
            Free(name);
            ExceptRethrow();
        }
        Free(from);
        Free(to);
        Free(obj->name);
        obj->name = name;
        return;
    }

    ExceptTry
    {
        mb = VCBuffer(obj->svr->vc);
        MBSetCom(mb, SMBmv);
        vwv[0] = obj->attribute;
        MBSetVWV(mb, 1, vwv);
        MBAddAscii(mb, obj->path);
        MBContinueAscii(mb, obj->name);
        MBAddAscii(mb, obj->path);
        MBContinueAscii(mb, name);
        // DiagPrint("Rename: %s%s to %s%s\n", obj->path, obj->name, obj->path, name);

        communicate(obj->svr);
        Free(obj->name);
        obj->name = name;
    }
    ExceptCatch
    {
        Free(name);
        ExceptRethrow();
    }
}


void SMBMove(smb_obj_t obj1, smb_obj_t obj2)
{
    data_t mb;
    ushort vwv[1];

    if(obj1->svr->smb2)
    {
        char *from, *to;

        /* The rename replaces the target itself, so it is not deleted
           first the way the older request needed */
        from = SMB2Path(obj1->path, obj1->name);
        to = NULL;
        ExceptTry
        {
            to = SMB2Path(obj2->path, obj2->name);
            SMB2Rename((smb2_conn_t) obj1->svr->smb2, from, to,
                       (obj1->attribute & SMB_SUBDIR) ? 1 : 0);
        }
        ExceptCatch
        {
            Free(from);
            if(to) Free(to);
            ExceptRethrow();
        }
        Free(from);
        Free(to);
        obj1->dirty = 2;    /* Mark as not needing deleting */
        return;
    }

    SMBDelete(obj2);

    mb = VCBuffer(obj1->svr->vc);
    MBSetCom(mb, SMBmv);
    vwv[0] = obj1->attribute;
    MBSetVWV(mb, 1, vwv);
    MBAddAscii(mb, obj1->path);
    MBContinueAscii(mb, obj1->name);
    MBAddAscii(mb, obj2->path);
    MBContinueAscii(mb, obj2->name);
    // DiagPrint("Move: %s%s to %s%s\n", obj1->path, obj1->name, obj2->path, obj2->name);

    communicate(obj1->svr);
    obj1->dirty = 2; /* Mark as not needing deleting */
}


void SMBDelete(smb_obj_t obj)
{
    data_t mb;
    ushort vwv[1];
    int is_dir;

    if(obj->dirty == 2)  /* Has been renamed */
        return;
    is_dir = (obj->attribute & SMB_SUBDIR);

    if(obj->svr->smb2)
    {
        char *path;

        path = SMB2Path(obj->path, obj->name);
        ExceptTry
        {
            SMB2Delete((smb2_conn_t) obj->svr->smb2, path, is_dir ? 1 : 0);
        }
        ExceptCatch
        {
            Free(path);
            ExceptRethrow();
        }
        Free(path);
        return;
    }

    mb = VCBuffer(obj->svr->vc);
    MBSetCom(mb, is_dir ? SMBrmdir : SMBunlink);
    vwv[0] = obj->attribute;
    MBSetVWV(mb, is_dir ? 0 : 1, vwv);
    MBAddAscii(mb, obj->path);
    MBContinueAscii(mb, obj->name);
    // DiagPrint("Delete: %s%s\n", obj->path, obj->name);

    communicate(obj->svr);
}


static char *cat_path(path_t path, smb_server_t svr)
{
    int i, len;
    char *cpath, *p;

    len = 1;
    for(i = 1; i < path->ntok-1; i++)
        len += strlen(path->tok[i])+1;
    p = cpath = Malloc(len+1);
    *p++ = '.';
    for(i = 1; i < path->ntok-1; i++)
    {
        strcpy(p, path->tok[i]);
        p += strlen(p);
        *p++ = '.';
    }
    *p++ = 0;
    ExceptTry
    {
        p = svr->ro2smb(cpath);
        Free(cpath);
    }
    ExceptCatch
    {
        Free(cpath);
        ExceptRethrow();
    }
    return p;
}

static char *leaf_of_path(path_t path, smb_server_t svr, char *pattern)
{
    char *p, *res;

    p = Malloc(strlen(path->tok[path->ntok-1]) + strlen(pattern) + 1);
    strcpy(p, path->tok[path->ntok-1]);
    strcat(p, pattern);
    ExceptTry
    {
        res = svr->ro2smb(p);
    }
    ExceptCatch
    {
        Free(p);
        ExceptRethrow();
    }
    Free(p);
    return res;
}

smb_obj_t SMBPath2Obj(path_t path, smb_server_t svr, char *pattern)
{
    smb_obj_t obj;

    obj = svr->construct(svr);
    obj->path = cat_path(path, svr);
    obj->name = leaf_of_path(path, svr, pattern);
    ExceptTry
    {
        if(!(*pattern ? svr->first(obj, obj->name) : svr->form(obj, obj->name)))
        {
            SMBObjDestruct(obj);
            obj = NULL;
        }
    }
    ExceptCatch
    {
        SMBObjDestruct(obj);
        ExceptRethrow();
    }
    return obj;
}

smb_obj_t SMBRoot(smb_server_t svr)
{
    smb_obj_t obj;

    obj = svr->construct(svr);
    obj->length = 0;
    obj->attribute = SMB_ROOT_ATTRS;
    obj->time = 0;
    return obj;
}

smb_obj_t SMBIn(smb_obj_t obj)
{
    // printf("In\n");
    extend_path(obj);
    if(!obj->svr->first(obj, obj->svr->dialect < NT ? "*.*" : "*"))
    {
        SMBObjDestruct(obj);
        return NULL;
    }
    obj->dirty = 0;
    return obj;
}


smb_obj_t SMBNext(smb_obj_t obj)
{
    // printf("Next\n");
    if(!obj->svr->next(obj))
    {
        SMBObjDestruct(obj);
        return NULL;
    }
    obj->dirty = 0;
    return obj;
}


smb_obj_t SMBObjDup(smb_obj_t obj)
{
    // printf("Dup\n");
    return obj->svr->dup(obj);
}


void SMBObjDestruct(smb_obj_t obj)
{
    obj->svr->destruct(obj);
}


char *SMBGetName(smb_obj_t obj)
{
    if(obj->path == NULL) return "ROOT";
    if(obj->ro_name)
    {
        Free(obj->ro_name);
        obj->ro_name = NULL;
    }
    obj->ro_name = obj->svr->smb2ro(obj->name);
    return obj->ro_name;
}


int SMBGetLength(smb_obj_t obj)
{
    return obj->length;
}


void SMBSetLength(smb_obj_t obj, int l)
{
    // DiagPrint("Set length: %s%s to %d\n", obj->path, obj->name, l);
    obj->length = l;
}


int SMBGetAttributes(smb_obj_t obj)
{
    return obj->attribute;
}


void SMBSetAttributes(smb_obj_t obj, int attr)
{
    // DiagPrint("Set attr: %s%s to 0x%08x\n", obj->path, obj->name, attr);
    obj->attribute = attr;
    obj->dirty = 1;
}


int SMBGetTime(smb_obj_t obj)
{
    return obj->time;
}


void SMBSetTime(smb_obj_t obj, int t)
{
    // DiagPrint("Set time: %s%s to %d\n", obj->path, obj->name, t);
    obj->time = t;
    obj->dirty = 1;
}

void SMBFlush(smb_obj_t obj)
{
    // DiagPrint("Flush: %s%s\n", obj->path, obj->name);
    obj->svr->flush(obj);
}

#ifdef DODIFF
static FILE *test_file = NULL;
static FILE *results = NULL;
static char *tbuf = NULL;
static int tbuf_size = 2048;

static void test_init(void)
{
    test_file = fopen("<LanMan98$Dir>.TestFile", "r");
    results = fopen("<LanMan98$Dir>.Results", "w");
}

static void test_fin(void)
{
    if(test_file) fclose(test_file);
    if(results) fclose(results);
    test_file = NULL;
    results = NULL;
    Free(tbuf);
    tbuf = NULL;
}

static void test_compare(char *buf, int pos, int count)
{
    if(test_file == NULL || results == NULL)
        return;
    if(tbuf == NULL || count > tbuf_size)
    {
        Free(tbuf);
        if(count > tbuf_size)
            tbuf_size = count;
        tbuf = Malloc(tbuf_size);
    }
    fseek(test_file, pos, SEEK_SET);
    fread(tbuf, 1, count, test_file);
    fprintf(results, "Reading 0x%x from 0x%x\n", count, pos);
    if(memcmp(buf, tbuf, count) != 0)
    {
        int i;

        for(i = 0; i < count; i++)
            if(tbuf[i] != buf[i])
                fprintf(results, "Error at %04x: %02x -> %02x\n", i, tbuf[i], buf[i]);
    }
}
#endif

smb_handle_t SMBOpen(smb_obj_t obj, int update)
{
    smb_handle_t h;
    data_t mb;
    ushort vwv[15];
    int fid;

    if(obj->svr->smb2)
    {
        char *path;

        path = SMB2Path(obj->path, obj->name);
        h = Malloc(sizeof(*h));
        memset(h, 0, sizeof(*h));
        h->svr = obj->svr;
        ExceptTry
        {
            SMB2Open((smb2_conn_t) obj->svr->smb2, path, 0, update,
                     h->fileid, NULL, NULL, NULL, NULL, NULL);
        }
        ExceptCatch
        {
            Free(path);
            Free(h);
            ExceptRethrow();
        }
        Free(path);
        h->max_rw_size = (int) SMB2MaxRead((smb2_conn_t) obj->svr->smb2);
        VCLog("     opened %s%s, transfers in %d byte pieces",
              obj->path ? obj->path : "", obj->name ? obj->name : "",
              h->max_rw_size);
        if(h->max_rw_size <= 0)
            Error("The SMB2 connection reports no usable transfer size");
        return h;
    }

    #ifdef DODIFF
    test_init();
    #endif
    mb = VCBuffer(obj->svr->vc);
    // DiagPrint("Open: %s%s for %s\n", obj->path, obj->name, update ? "update" : "read");

    if (obj->svr->dialect >= LM1)
    {
        MBSetCom(mb, SMBopenandx);
        vwv[0] = 0xff;
        vwv[1] = 0;
        vwv[2] = 0;
        vwv[3] = update ? 0x12 : 0x20;
        vwv[4] = obj->attribute;
        vwv[5] = 0;
        vwv[6] = 0;
        vwv[7] = 0;
        vwv[8] = 0x01;
        vwv[9] = 0;
        vwv[10] = 0;
        vwv[11] = 0;
        vwv[12] = 0;
        vwv[13] = 0;
        vwv[14] = 0;
        MBSetVWV(mb, 15, vwv);
        MBAddRaw(mb, strlen(obj->path), obj->path);
        MBAddRaw(mb, strlen(obj->name) + 1, obj->name);

        communicate(obj->svr);

        MBGetVWV(mb, 3, vwv);
        fid = vwv[2];
    }
    else
    {
        MBSetCom(mb, SMBopen);
        vwv[0] = update ? 0x12 : 0x20;
        vwv[1] = obj->attribute;
        MBSetVWV(mb, 2, vwv);
        MBAddAscii(mb, obj->path);
        MBContinueAscii(mb, obj->name);

        communicate(obj->svr);

        MBGetVWV(mb, 1, vwv);
        fid = vwv[0];
    }

    h = Malloc(sizeof(*h));
    memset(h, 0, sizeof(*h));
    h->svr = obj->svr;
    h->fid = fid;
    h->max_rw_size = (mb.size - HDRSIZE - 20) & ~3;
    VCLog("     opened %s%s, transfers in %d byte pieces",
          obj->path ? obj->path : "", obj->name ? obj->name : "",
          h->max_rw_size);
    return h;
}

smb_handle_t SMBOpenPrinter(smb_server_t svr)
{
    smb_handle_t h;
    data_t mb;
    ushort vwv[2];

    if(svr->smb2)
    {
        static int job = 0;
        char name[64];
        unsigned int max;

        /*
            SMB2 has no print command.  A print share takes a job as an
            ordinary file: create it, write the data, close it, and the
            server spools what was written.  The name is what the queue
            shows and is never read back, so it only has to be
            recognisable and different from the last one.
        */
        job++;
        sprintf(name, "RISC OS job %d", job);

        h = Malloc(sizeof(*h));
        memset(h, 0, sizeof(*h));
        h->svr = svr;
        h->fid = 0;
        h->printing = 1;
        ExceptTry
        {
            SMB2OpenPrint((smb2_conn_t) svr->smb2, name, h->fileid);
        }
        ExceptCatch
        {
            Free(h);
            ExceptRethrow();
        }
        max = SMB2MaxWrite((smb2_conn_t) svr->smb2);
        if(max == 0)
            max = 4096;
        h->max_rw_size = (int) max;
        return h;
    }

    mb = VCBuffer(svr->vc);
    MBSetCom(mb, SMBopenPrintFile);
    vwv[0] = 0;
    vwv[1] = 1;
    MBSetVWV(mb, 2, vwv);
    MBAddAscii(mb, "");

    communicate(svr);

    MBGetVWV(mb, 1, vwv);
    h = Malloc(sizeof(*h));
    memset(h, 0, sizeof(*h));
    h->svr = svr;
    h->fid = vwv[0];
    h->printing = 1;
    h->max_rw_size = (mb.size - HDRSIZE - 20) & ~3;
    // h->max_rw_size = (mb.size - HDRSIZE - 16) & ~3;
    // printf("OpenPrintFile\n");
    return h;
}

static void flush_held(smb_handle_t h, int trim_to);

void SMBClose(smb_handle_t h)
{
    data_t mb;
    ushort vwv[3];

    /*
        Anything still held goes out whole: the length was never set, so
        there is nothing to say which part of it was padding.
    */
    if(h && h->printing)
    {
        flush_held(h, -1);
        if(h->hold)
        {
            Free(h->hold);
            h->hold = NULL;
        }
    }
    if(h && h->svr->smb2)
    {
        SMB2Close((smb2_conn_t) h->svr->smb2, h->fileid);
        Free(h);
        return;
    }
    if(h)
    {
        mb = VCBuffer(h->svr->vc);
        MBSetCom(mb, SMBclose);
        vwv[0] = h->fid;
        vwv[1] = (ushort) -1;
        vwv[2] = (ushort) -1;
        MBSetVWV(mb, 3, vwv);
        // DiagPrint("Close\n");

        communicate(h->svr);

        Free(h);
    }
#ifdef DODIFF
    test_fin();
#endif
    // printf("Close\n");
}


/*
    READ_ANDX.  Unlike the core read, the response says where it put the
    payload rather than implying it from position, so the length and offset
    both come out of the reply.  A server is allowed to return less than was
    asked for, which the loop handles; the core read simply assumed it got
    everything it wanted.
*/
static void read_andx(void *buf, int pos, int count, smb_handle_t h)
{
    smb_server_t svr;
    data_t mb;
    ushort vwv[12];
    char *cbuf;
    int size, len, off;

    svr = h->svr;
    cbuf = buf;
    while(count)
    {
        mb = VCBuffer(svr->vc);
        size = MIN(svr->max_xfer, count);
        MBSetCom(mb, SMBreadX);
        vwv[0] = 0xFF;              /* nothing chained after this */
        vwv[1] = 0;
        vwv[2] = h->fid;
        vwv[3] = pos;
        vwv[4] = (pos >> 16);
        vwv[5] = size;              /* MaxCountOfBytesToReturn */
        vwv[6] = size;              /* MinCountOfBytesToReturn */
        vwv[7] = 0;                 /* Timeout, and MaxCountHigh with it */
        vwv[8] = 0;
        vwv[9] = (count > 0xFFFF) ? 0xFFFF : count;   /* Remaining: advisory */
        vwv[10] = 0;                /* OffsetHigh.  The interface above this
                                       carries a 32 bit position, so there is
                                       nothing yet to put here. */
        vwv[11] = 0;
        MBSetVWV(mb, 12, vwv);

        communicate(svr);

        mb = VCBuffer(svr->vc);
        MBGetVWV(mb, 12, vwv);
        len = vwv[5] | (vwv[7] << 16);
        off = vwv[6];
        if(len > size)
            len = size;
        if(len > 0)
            memcpy(cbuf, MBAt(mb, off, len), len);

        if(len < size)
        {
            /*
                Short of what was asked for, which means the end of the
                file: the layer above routinely asks for a whole block and
                sorts out the length itself.  The rest of the caller's
                buffer is zeroed rather than left as it was, because what
                was there is the last thing this connection happened to
                read - possibly from another file.
            */
            if(len == 0)
                VCLog("*    read at %d returned nothing; %d bytes "
                      "zero filled", pos, count);
            memset(cbuf + len, 0, count - len);
            return;
        }

        count -= len;
        pos += len;
        cbuf += len;
    }
}

static void write_andx(void *buf, int pos, int count, smb_handle_t h)
{
    smb_server_t svr;
    data_t mb;
    ushort vwv[14];
    char *cbuf;
    int size, wrote;

    svr = h->svr;
    cbuf = buf;
    while(count)
    {
        mb = VCBuffer(svr->vc);
        size = MIN(svr->max_xfer, count);
        MBSetCom(mb, SMBwriteX);
        vwv[0] = 0xFF;
        vwv[1] = 0;
        vwv[2] = h->fid;
        vwv[3] = pos;
        vwv[4] = (pos >> 16);
        vwv[5] = 0;                 /* Timeout */
        vwv[6] = 0;
        vwv[7] = 0;                 /* WriteMode: no write-through, no raw */
        vwv[8] = (count > 0xFFFF) ? 0xFFFF : count;   /* Remaining */
        vwv[9] = 0;                 /* DataLengthHigh */
        vwv[10] = size;             /* DataLength */
        vwv[11] = MBDataOffset(14); /* where the payload lands below */
        vwv[12] = 0;                /* OffsetHigh, as above */
        vwv[13] = 0;
        MBSetVWV(mb, 14, vwv);
        MBAddRaw(mb, size, cbuf);

        communicate(svr);

        mb = VCBuffer(svr->vc);
        MBGetVWV(mb, 6, vwv);
        wrote = vwv[2];
        if(wrote <= 0)
            Error("Write stored no data");
        if(wrote > size)
            wrote = size;

        count -= wrote;
        pos += wrote;
        cbuf += wrote;
    }
}

void SMBRead(void *buf, int pos, int count, smb_handle_t h)
{
    smb_server_t svr;
    data_t mb;
    smb_buf_t smb_buf;
    ushort vwv[8];
    char *cbuf;
    int size, requested_size;
    int reading_raw;

    svr = h->svr;
    if(svr->smb2)
    {
        char *cb = buf;
        int got, first = 1;

        while(count > 0)
        {
            int ask = MIN(h->max_rw_size, count);

            got = SMB2Read((smb2_conn_t) svr->smb2, h->fileid,
                           (unsigned int) pos, 0, (unsigned char *) cb, ask);
            if(got <= 0)
            {
                /*
                    End of file.  As on the SMB1 path, the rest of the
                    caller's buffer is zeroed rather than left as it was.

                    Zero filling the tail of the last block is ordinary -
                    the layer above asks for a whole block and sorts the
                    length out itself.  Getting nothing at all on the very
                    first try is not: the caller asked for data that ought
                    to be there and is handed zeros, which it cannot tell
                    from a file that really is zeros.  An archive read that
                    way looks like an empty one rather than a failed one,
                    so say it happened.
                */
                if(first)
                    VCLog("*    read at %d returned nothing; %d bytes "
                          "zero filled", pos, count);
                memset(cb, 0, count);
                return;
            }
            first = 0;
            count -= got;
            pos += got;
            cb += got;
            if(got < ask)
            {
                /*
                    Short of what was asked for, which here means the file
                    ended: the server returns everything it has up to the
                    amount requested, and the amount requested was never
                    more than it said it would accept.

                    Going round again to be told END_OF_FILE costs a whole
                    round trip to learn what this already knows, and it is
                    paid on the last read of every file that does not end
                    exactly on a block.  Invisible on a local network and
                    the length of the link away from it.
                */
                memset(cb, 0, count);
                return;
            }
        }
        return;
    }
    if(svr->use_andx)
    {
        read_andx(buf, pos, count, h);
        return;
    }
    mb = VCBuffer(svr->vc);
    cbuf = buf;
    reading_raw = svr->max_raw_read;
    while(count && reading_raw)
    {
        requested_size = MIN(svr->max_raw_read, count);
        MBSetCom(mb, SMBreadRaw);
        vwv[0] = h->fid;
        vwv[1] = pos;
        vwv[2] = (pos >> 16);
        vwv[3] = requested_size;
        vwv[4] = 0;
        vwv[5] = 0;
        vwv[6] = 0;
        vwv[7] = 0;
        MBSetVWV(mb, 8, vwv);

        svr->send(svr->vc);

        size = VCReceiveRaw(cbuf, requested_size, svr->vc);
        reading_raw = (size == requested_size);

        count -= size;
        pos += size;
        cbuf += size;
    }
    while(count)
    {
        size = MIN(h->max_rw_size, count);
        MBSetCom(mb, SMBread);
        vwv[0] = h->fid;
        vwv[1] = size;
        vwv[2] = pos;
        vwv[3] = (pos >> 16);
        vwv[4] = count;
        MBSetVWV(mb, 5, vwv);

        communicate(svr);

        MBGetBuf(mb, &smb_buf);
        if(MBBufType(&smb_buf) != DATA)
            Error("Read didn't return data");
        memcpy(cbuf, MBBufData(&smb_buf), size); /* Check size ??? */
#ifdef DODIFF
        test_compare(cbuf, pos, size);
        fprintf(results, "Requested 0x%x, received 0x%x\n", size, MBBufSize(&smb_buf));
#endif

        count -= size;
        pos += size;
        cbuf += size;
    }
}


static void write_data(void *buf, int pos, int count, smb_handle_t h)
{
    data_t mb;
    ushort vwv[5];
    char *cbuf;
    int size;

    if(h->svr->smb2)
    {
        char *cb = buf;
        int put;

        while(count > 0)
        {
            put = SMB2Write((smb2_conn_t) h->svr->smb2, h->fileid,
                            (unsigned int) pos, 0, (unsigned char *) cb,
                            MIN(h->max_rw_size, count));
            if(put <= 0)
                Error("Write stored no data");
            count -= put;
            pos += put;
            cb += put;
        }
        return;
    }
    if(h->svr->use_andx)
    {
        write_andx(buf, pos, count, h);
        return;
    }
    mb = VCBuffer(h->svr->vc);
    cbuf = buf;
    while(count)
    {
        // size = MIN(MIN(h->max_rw_size, count), 4*1024);
        size = MIN(h->max_rw_size, count);
        MBSetCom(mb, SMBwrite);
        vwv[0] = h->fid;
        vwv[1] = size;
        vwv[2] = pos;
        vwv[3] = (pos >> 16);
        vwv[4] = count;
        MBSetVWV(mb, 5, vwv);
        MBAddData(mb, size, cbuf);

        communicate(h->svr);

        count -= size;
        pos += size;
        cbuf += size;
    }
}

/*
    Send whatever is being held, optionally cut short.

    trim_to is the length the whole job is to end up, or -1 to send the held
    block whole.  Anything of it that falls beyond that length is padding
    the layer above put there and is dropped.
*/
static void flush_held(smb_handle_t h, int trim_to)
{
    int n;

    if((h->hold == NULL) || (h->hold_len <= 0))
        return;
    n = h->hold_len;
    if(trim_to >= 0)
    {
        n = trim_to - h->hold_pos;
        if(n < 0) n = 0;
        if(n > h->hold_len) n = h->hold_len;
    }
    h->hold_len = 0;            /* cleared first: write_data can throw */
    if(n > 0)
        write_data(h->hold, h->hold_pos, n, h);
}

/*
    Write to a file, or to a print job.

    A file can be written and then cut to length afterwards, which is what
    the layer above does: it writes whole buffers and then says how much of
    the last one was real.  A print job cannot - a share that spools has no
    length to set, and says so - and the padding at the end of the last
    buffer would come out of the printer as rubbish.

    So for a print job the most recent block is held rather than sent.  The
    next write releases it; the call that sets the length releases it cut to
    size.  Only one block is ever held, which is all it takes, because only
    the last one is ever short.
*/
void SMBWrite(void *buf, int pos, int count, smb_handle_t h)
{
    if(!h->printing)
    {
        write_data(buf, pos, count, h);
        return;
    }

    flush_held(h, -1);          /* the one before this is now known good */

    if(count <= 0)
        return;
    if(count > h->hold_max)
    {
        if(h->hold) Free(h->hold);
        h->hold = Malloc(count);
        h->hold_max = count;
    }
    memcpy(h->hold, buf, count);
    h->hold_pos = pos;
    h->hold_len = count;
}

void SMBSetExtent(smb_handle_t h, int length)
{
    data_t mb;
    ushort vwv[5];

    /*
        A print job is not cut to length afterwards - a share that spools
        refuses to be, and answers that the path is invalid.  Instead the
        length is what decides how much of the held block goes out.
    */
    if(h->printing)
    {
        flush_held(h, length);
        return;
    }

    if(h->svr->smb2)
    {
        SMB2SetEnd((smb2_conn_t) h->svr->smb2, h->fileid,
                   (unsigned int) length, 0);
        return;
    }

    ExceptTry
    {
        mb = VCBuffer(h->svr->vc);
        MBSetCom(mb, SMBwrite);
        vwv[0] = h->fid;
        vwv[1] = 0;
        vwv[2] = length;
        vwv[3] = (length >> 16);
        vwv[4] = 0;
        MBSetVWV(mb, 5, vwv);
        MBAddData(mb, 0, NULL);

        communicate(h->svr);
    }
    ExceptCatch
    {
        // Some NASes don't support a write of zero length, so write one byte
        // beyond the end of the file. The size will be set back to the length
        // when the file is closed.
        char data = 0;

        mb = VCBuffer(h->svr->vc);
        MBSetCom(mb, SMBwrite);
        vwv[0] = h->fid;
        vwv[1] = 1;
        vwv[2] = length;
        vwv[3] = (length >> 16);
        vwv[4] = 0;
        MBSetVWV(mb, 5, vwv);
        MBAddData(mb, 1, &data);

        communicate(h->svr);
    }
}
