/*
 * 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
 *   Portions Copyright RISC OS Developments 2019+, credited to the RISC OS One Project.
 */

/*
 *   The filing system's view of an SMB2 connection.
 *
 *   c.lm2 does this job for the LANMAN2 and NT dialects of SMB1 by filling
 *   in the function table in h.smbsvr; this fills in the same table with
 *   SMB2 equivalents, so everything above it - the object model, the
 *   directory walk, the RISC OS filing system entry points - carries on
 *   unchanged.
 *
 *   Two things differ enough to be worth naming.  A directory enumeration
 *   is held open across the whole walk here, where SMB1 could hand back a
 *   search identifier and forget about it, so the object owns a handle
 *   that has to be closed however the walk ends.  And a path is relative
 *   to the share with no leading separator, where SMB1 wanted one.
 */

#include <stdio.h>
#include <string.h>

#include "LanMan98BaseLib/error.h"
#include "LanMan98BaseLib/memory.h"
#include "LanMan98BaseLib/strext.h"
#include "date.h"
#include "lm2.h"
#include "smb.h"
#include "alphabet.h"
#include "smb2.h"
#include "smbsvr.h"

#include "smb2fs.h"

/* One bufferful of directory entries per exchange */
#define DIRBUFSIZE (16 * 1024)

/* FileBothDirectoryInformation, the same layout SMB1's find level 0x104
   returns, so what c.lm2 decodes and what this decodes are one thing. */
#define ENT_NEXT       (0)
#define ENT_WRITE_LOW  (24)
#define ENT_WRITE_HIGH (28)
#define ENT_SIZE_LOW   (40)
#define ENT_ATTRS      (56)
#define ENT_NAMELEN    (60)
#define ENT_NAME       (94)

/*
    An open directory, shared between an object and any duplicate of it.

    The layer above duplicates an object part way through a walk and then
    carries on with the copy, so a duplicate has to be able to continue.
    That means it needs the entries already fetched - it gets its own copy
    of the buffer and its own position in it - and it needs the handle the
    next fetch will come from, which is this, counted so that whichever
    object goes last is the one that closes it.

    The SMB1 side has always worked this way; its search identifier is
    counted for the same reason.  Not doing the same here is what made a
    directory of more than one bufferful stop part way through.
*/
typedef struct dirh_s
{
    unsigned char id[SMB2_FILEID_LEN];
    int refs;
} *dirh_t;

struct smb_obj_s
{
#include "smbobj.h"
    dirh_t dirh;                /* NULL when no walk is in progress */
    unsigned char *dir_buf;
    int dir_len;
    int dir_index;
    int dir_more;
    char *pattern;
};

static unsigned int get32(const unsigned char *p)
{
    return ((unsigned int) p[0]) | (((unsigned int) p[1]) << 8) |
           (((unsigned int) p[2]) << 16) | (((unsigned int) p[3]) << 24);
}

static smb2_conn_t conn(smb_obj_t obj)
{
    return (smb2_conn_t) obj->svr->smb2;
}

/*
    The path an SMB2 request wants: what is below the share, with no
    leading separator.  obj->path is already in SMB form and starts with
    one, and the share root is the empty string rather than "\\".
*/
char *SMB2Path(char *path, char *name)
{
    char *p;
    int n;

    if(path == NULL) path = "";
    if(name == NULL) name = "";
    while(*path == '\\') path++;

    n = strlen(path) + strlen(name);
    p = Malloc(n + 1);
    strcpy(p, path);
    strcat(p, name);

    /* A trailing separator names the same directory and confuses some
       servers, so it goes */
    n = strlen(p);
    while((n > 0) && (p[n - 1] == '\\'))
        p[--n] = 0;
    return p;
}

static void close_dir(smb_obj_t obj)
{
    if(obj->dirh)
    {
        if(--obj->dirh->refs <= 0)
        {
            SMB2Close(conn(obj), obj->dirh->id);
            Free(obj->dirh);
        }
        obj->dirh = NULL;
    }
    if(obj->dir_buf)
    {
        Free(obj->dir_buf);
        obj->dir_buf = NULL;
    }
    obj->dir_len = 0;
    obj->dir_index = 0;
    obj->dir_more = 0;
    if(obj->pattern)
    {
        Free(obj->pattern);
        obj->pattern = NULL;
    }
}

/* --- the object model --- */

static smb_obj_t construct(smb_server_t svr)
{
    smb_obj_t obj;

    obj = Malloc(sizeof(*obj));
    memset(obj, 0, sizeof(*obj));
    obj->svr = svr;
    return obj;
}

static smb_obj_t dup(smb_obj_t obj)
{
    smb_obj_t nobj;

    nobj = construct(obj->svr);
    ExceptTry
    {
        if(obj->path) nobj->path = strdup(obj->path);
        if(obj->name) nobj->name = strdup(obj->name);
        nobj->length = obj->length;
        nobj->attribute = obj->attribute;
        nobj->time = obj->time;
        nobj->dirty = obj->dirty;

        /*
            Carry the walk over.  The entries already fetched are copied,
            so each object has its own position in them, and the handle
            the next fetch will use is shared and counted.
        */
        if(obj->pattern) nobj->pattern = strdup(obj->pattern);
        if(obj->dir_buf && (obj->dir_len > 0))
        {
            nobj->dir_buf = Malloc(DIRBUFSIZE);
            memcpy(nobj->dir_buf, obj->dir_buf, (size_t) obj->dir_len);
            nobj->dir_len = obj->dir_len;
            nobj->dir_index = obj->dir_index;
        }
        if(obj->dirh)
        {
            obj->dirh->refs++;
            nobj->dirh = obj->dirh;
        }
    }
    ExceptCatch
    {
        if(nobj->dir_buf) Free(nobj->dir_buf);
        if(nobj->pattern) Free(nobj->pattern);
        if(nobj->path) Free(nobj->path);
        if(nobj->name) Free(nobj->name);
        Free(nobj);
        ExceptRethrow();
    }
    return nobj;
}

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

/* Fill in an object from what a create told us about it */
static void set_vals(smb_obj_t obj, unsigned int size, unsigned int attrs,
                     unsigned int wlow, unsigned int whigh)
{
    obj->length = (int) size;
    obj->attribute = (int) (attrs & 0xFF);
    obj->time = DateTimetoSMB(wlow, (int) whigh);
}

/* Stat one named object, whichever kind it turns out to be */
static int form(smb_obj_t obj, char *name)
{
    unsigned char fileid[SMB2_FILEID_LEN];
    unsigned int size_low, size_high, attrs, wl, wh;
    char *path;
    volatile int found;

    path = SMB2Path(obj->path, name);
    found = 0;
    ExceptTry
    {
        SMB2Open(conn(obj), path, -1, 0, fileid,
                 &size_low, &size_high, &attrs, &wl, &wh);
        SMB2Close(conn(obj), fileid);
        set_vals(obj, size_low, attrs, wl, wh);
        found = 1;
    }
    ExceptCatch
    {
        /* Not there is an answer, not a failure: the layer above asks
           about names that may not exist and reads the result. */
        int errnum = ExceptCaught()->errnum;

        if((errnum != 0x10002) && (errnum != 0x10003))
        {
            Free(path);
            ExceptRethrow();
        }
    }
    Free(path);
    if(found)
        obj->dirty = 0;
    return found;
}

/*
    Decode the entry the walk is sitting on, and step past it.  Returns 0
    when the buffer is spent, which is the caller's cue to fetch more.
*/
static int take_entry(smb_obj_t obj)
{
    unsigned char *e;
    unsigned int next, attrs;
    int namelen, i, j;

    if(obj->dir_buf == NULL) return 0;
    if(obj->dir_index + ENT_NAME > obj->dir_len) return 0;

    e = obj->dir_buf + obj->dir_index;
    next = get32(e + ENT_NEXT);
    namelen = (int) get32(e + ENT_NAMELEN);
    if(namelen < 0) namelen = 0;
    if(obj->dir_index + ENT_NAME + namelen > obj->dir_len)
        return 0;

    if(obj->name) { Free(obj->name); obj->name = NULL; }
    obj->name = Malloc(namelen / 2 + 1);
    /*
        UTF-16LE on the wire, converted back through the alphabet rather
        than by taking the low byte of each unit: the characters RISC OS
        keeps at &80-&9F arrive as code points nowhere near those values,
        and the low byte of one of those is not the character but a piece
        of it.  Anything the alphabet cannot represent - which is most of
        Unicode - becomes an underscore.  The name then no longer matches
        the one on the server, so the file can be seen but not opened;
        that is the best a fixed alphabet can do, and it is better than a
        full stop, which RISC OS would read as part of the path.

        A surrogate pair is a character outside the basic plane.  It can
        never be in the alphabet, so it is one underscore, not two.
    */
    for(i = 0, j = 0; i * 2 < namelen; i++)
    {
        unsigned int ch, ro;

        ch = e[ENT_NAME + i * 2] | (((unsigned int) e[ENT_NAME + i * 2 + 1]) << 8);
        if((ch >= 0xD800) && (ch <= 0xDBFF))
        {
            if((i + 1) * 2 < namelen) i++;      /* step over the low half */
            obj->name[j++] = '_';
            continue;
        }
        ro = AlphabetFromUnicode(ch);
        obj->name[j++] = (char) (ro ? ro : '_');
    }
    obj->name[j] = 0;

    attrs = get32(e + ENT_ATTRS);
    set_vals(obj, get32(e + ENT_SIZE_LOW), attrs,
             get32(e + ENT_WRITE_LOW), get32(e + ENT_WRITE_HIGH));

    if(next == 0)
        obj->dir_index = obj->dir_len;      /* last one in this bufferful */
    else
        obj->dir_index += (int) next;
    return 1;
}

/* Fetch the next bufferful, returning 0 once the server has no more */
static int fill_dir(smb_obj_t obj, int restart)
{
    int n, want;
    unsigned int limit;

    if(obj->dirh == NULL)
        return 0;
    if(obj->dir_buf == NULL)
        obj->dir_buf = Malloc(DIRBUFSIZE);

    /*
        Ask for no more than the server said it would send, and no more
        than the packet buffer can hold.  A server advertising less than
        this wants refuses the request outright; a machine whose heap would
        not stretch to the buffer this wanted receives a reply it has
        nowhere to put.  Neither may be assumed from the other, and both
        were being ignored.
    */
    want = DIRBUFSIZE;
    limit = SMB2MaxTransact(conn(obj));
    if((limit > 0) && ((int) limit < want))
        want = (int) limit;

    n = SMB2QueryDir(conn(obj), obj->dirh->id,
                     obj->pattern ? obj->pattern : "*", restart,
                     obj->dir_buf, want);
    obj->dir_len = n;
    obj->dir_index = 0;
    return n > 0;
}

static int first(smb_obj_t obj, char *name)
{
    char *path;
    volatile int ok;

    close_dir(obj);

    path = SMB2Path(obj->path, NULL);
    ok = 0;
    ExceptTry
    {
        dirh_t h;

        h = Malloc(sizeof(*h));
        h->refs = 1;
        SMB2Open(conn(obj), path, 1, 0, h->id,
                 NULL, NULL, NULL, NULL, NULL);
        obj->dirh = h;
        ok = 1;
    }
    ExceptCatch
    {
        int errnum = ExceptCaught()->errnum;

        Free(path);
        if((errnum != 0x10002) && (errnum != 0x10003))
            ExceptRethrow();
        return 0;
    }
    Free(path);
    if(!ok) return 0;

    obj->pattern = strdup((name && *name) ? name : "*");

    ExceptTry
    {
        if(!fill_dir(obj, 1) || !take_entry(obj))
        {
            close_dir(obj);
            ok = 0;
        }
    }
    ExceptCatch
    {
        close_dir(obj);
        ExceptRethrow();
    }
    if(ok)
        obj->dirty = 0;
    return ok;
}

static int next(smb_obj_t obj)
{
    volatile int ok;

    if(obj->dirh == NULL) return 0;

    ok = 1;
    ExceptTry
    {
        if(!take_entry(obj))
        {
            if(!fill_dir(obj, 0) || !take_entry(obj))
            {
                close_dir(obj);
                ok = 0;
            }
        }
    }
    ExceptCatch
    {
        close_dir(obj);
        ExceptRethrow();
    }
    if(ok)
        obj->dirty = 0;
    return ok;
}

/*
    Write back whatever the layer above changed about an object.  Nothing
    is written back yet, so an object that was only read is unaffected and
    one that was altered says so rather than appearing to have been saved.
*/
static void flush(smb_obj_t obj)
{
    unsigned int ft_low, ft_high, attrs;
    char *path;

    if(!obj->dirty)
        return;
    if(obj->dirty == 2)     /* renamed away; nothing here to write back */
        return;

    DateSecondsToFileTime((unsigned int) obj->time, &ft_low, &ft_high);
    attrs = (unsigned int) (obj->attribute & 0xFF);
    if(attrs == 0)
        attrs = 0x80;       /* FILE_ATTRIBUTE_NORMAL: zero means no change */

    path = SMB2Path(obj->path, obj->name);
    ExceptTry
    {
        SMB2SetBasic(conn(obj), path,
                     (obj->attribute & SMB_SUBDIR) ? 1 : 0,
                     ft_low, ft_high, attrs);
    }
    ExceptCatch
    {
        Free(path);
        ExceptRethrow();
    }
    Free(path);
    obj->dirty = 0;
}

static void disk_info(smb_server_t svr, smb_disc_info_t *info)
{
    unsigned int total, avail, unit;

    SMB2FsInfo((smb2_conn_t) svr->smb2, &total, &avail, &unit);
    info->total = total;
    info->free = avail;
    info->units = unit;
}

/* SMB2 has no separate send and receive step for the layer above to drive */
static void no_send(vc_t vc)
{
    (void) vc;
    Error("An SMB1 request was built on an SMB2 connection");
}

smb_server_t SMB2Upgrade(smb_server_t svr)
{
    /*
        Take the long-name handling from the SMB1 upgrade first.  Without
        it the table still holds the core dialect's mapping, which upper
        cases every name and cuts it to eight and three - correct for a
        1987 server and quite wrong for this one.  Only the protocol
        functions are replaced below; the name mapping is the same job
        either way and is already written.
    */
    LM2Upgrade(svr, 1);

    svr->send      = no_send;
    svr->receive   = no_send;
    svr->construct = construct;
    svr->dup       = dup;
    svr->destruct  = destruct;
    svr->form      = form;
    svr->first     = first;
    svr->next      = next;
    svr->flush     = flush;
    svr->disk_info = disk_info;
    return svr;
}
