/*
 * 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.
 *
 * CDDL HEADER END
 */

/*
 *   Portions Copyright RISC OS Developments 2019+, credited to the RISC OS One Project.
 */

/*
 *   Portions Copyright RISC OS Developments 2019+, credited to the RISC OS One Project.
 */

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

#include "LanMan98BaseLib/strext.h"
#include "LanMan98BaseLib/error.h"
#include "LanMan98BaseLib/memory.h"

#include "smb.h"
#include "smbsvr.h"
#include "netbios.h"
#include "trans.h"
#include "var.h"
#include "word.h"
#include "vc.h"
#include "discover.h"
#include "browse.h"

/*
    NetServerEnum2, which is RAP function 104.

    "WrLehDz" describes the parameters and "B16BBDz" one entry of the
    answer, and both strings go on the wire: the server decodes what
    follows by reading them, which is why they have to be right to the
    letter rather than merely consistent with what this sends.

    Of the parameter letters only W, L, D and z put anything in the
    request - the level, the size of buffer being offered, which kinds of
    server are wanted and which workgroup - and the rest describe what
    comes back.
*/
#define RAP_NETSERVERENUM2  (104)
#define RAP_PARAM_DESC      "WrLehDz"
#define RAP_DATA_DESC       "B16BBDz"
#define RAP_LEVEL           (1)
#define RAP_ENTRY_LEN       (26)    /* B16 + B + B + D + z */
#define SV_TYPE_ALL         (0xFFFFFFFFu)

/*
    How much of an answer to ask for.  Each server costs 26 bytes and its
    comment, so this is room for a couple of hundred of them; a workgroup
    larger than that gets the first couple of hundred and a note that it
    was cut short, which is better than a module-sized buffer nobody has.
*/
#define RAP_DATA_MAX        (8192)

/*
    The name a machine holds when it is the master browser for a workgroup,
    and the one it holds when it is the domain controller.  The suffix is
    the sixteenth byte of the NetBIOS name; everything else is the
    workgroup, padded with spaces.
*/
#define NB_MASTER_BROWSER   (0x1D)
#define NB_DOMAIN_CONTROL   (0x1B)

static void nb_name(char *out, const char *wg, int suffix)
{
    int len;

    memset(out, ' ', 16);
    out[16] = 0;
    len = (int) strlen(wg);
    if(len > 15) len = 15;
    memcpy(out, wg, len);
    out[15] = (char) suffix;
}

/*
    Which workgroup to ask about.

    LanMan98$Workgroup if it is set, otherwise the domain the client
    authenticates in, which on a small network is the same thing, and
    "WORKGROUP" if neither says.
*/
static char *workgroup_name(char *given)
{
    char *var;

    if(given && *given)
        return strdup(given);
    var = VarRead("LanMan98$Workgroup");
    if(var == NULL)
        var = VarRead("LanMan98$Domain");
    if(var && *var)
        return var;
    if(var) Free(var);
    return strdup("WORKGROUP");
}

/*
    Find whoever is holding one of those names.

    The address comes back from a name query and the machine's own name
    from a node status sent to it - the browser name is a role, not a
    machine, and it is the machine's name that a connection needs.
*/
static char *holder_of(const char *wg, int suffix)
{
    char nb[17];
    char *volatile addr;
    char *volatile name;

    nb_name(nb, wg, suffix);
    addr = NULL;
    name = NULL;
    ExceptTry
    {
        addr = NetBIOSForceResolve(nb);
        if(addr)
            name = NetBIOSName(nb, addr);
    }
    ExceptCatch
    {
        /* Nothing holding the name is the ordinary case on a network that
           has no browser at all, and is not worth an error. */
        name = NULL;
    }
    if(addr) Free(addr);
    return name ? strdup(name) : NULL;
}

char *BrowseMaster(char *workgroup)
{
    char *wg, *name;

    wg = workgroup_name(workgroup);
    name = holder_of(wg, NB_MASTER_BROWSER);
    if(name == NULL)
        name = holder_of(wg, NB_DOMAIN_CONTROL);
    Free(wg);
    return name;
}

/*
    Take the answer apart.

    Each entry is a fixed 26 bytes: a sixteen byte name, two version bytes,
    what kind of server it says it is, and a pointer to its comment.  The
    comment is not taken - there is nowhere in a found_t to put it - so the
    converter that would turn that pointer into an offset is not needed
    either.
*/
static void take_entries(found_t *head, unsigned char *data, int data_len,
                         int count)
{
    unsigned char *e;
    char name[17];
    int i, off;

    for(i = 0; i < count; i++)
    {
        if((i + 1) * RAP_ENTRY_LEN > data_len)
            break;
        e = data + i * RAP_ENTRY_LEN;
        memcpy(name, e, 16);
        name[16] = 0;
        /* The name is padded with spaces or NULs depending on the server */
        for(off = 15; off >= 0 && (name[off] == ' ' || name[off] == 0); off--)
            name[off] = 0;
        if(name[0] == 0)
            continue;
        DiscoverAdd(head, name, NULL, 0);
    }
}

found_t BrowseServers(char *workgroup, char *user, char *passwd)
{
    /*
        Everything the catch touches is volatile.

        These are set inside the try and freed by the catch, and a local
        that a longjmp jumps back over is only as good as where the
        compiler happened to keep it.  Freeing a register's worth of
        leftovers is how this reads as a crash somewhere else entirely.
    */
    char *volatile wg;
    char *volatile master;
    char *volatile addr;
    smb_server_t volatile svr;
    trans_buf_t volatile parm;
    trans_buf_t volatile data;
    found_t volatile head;
    char *p;
    int len_pd, len_dd, len_wg, nparm;
    int status, count, total;

    wg = NULL;
    master = NULL;
    addr = NULL;
    svr = NULL;
    parm = NULL;
    data = NULL;
    head = NULL;

    wg = workgroup_name(workgroup);
    master = BrowseMaster((char *) wg);
    if(master == NULL)
    {
        Free((char *) wg);
        return NULL;            /* no browser on this network */
    }

    ExceptTry
    {
        addr = NetBIOSResolve((char *) master);
        if(addr == NULL)
            addr = strdup((char *) master);

        svr = SMBConnectBrowse((char *) addr, SMB_PORT_AUTO,
                               (char *) master, user, passwd);

        len_pd = (int) strlen(RAP_PARAM_DESC) + 1;
        len_dd = (int) strlen(RAP_DATA_DESC) + 1;
        len_wg = (int) strlen(wg) + 1;
        nparm = 2 + len_pd + len_dd + 2 + 2 + 4 + len_wg;

        parm = TransInitBuf(nparm, 8);
        data = TransInitBuf(0, RAP_DATA_MAX);

        p = ((trans_buf_t) parm)->buf;
        SetWord16(p, RAP_NETSERVERENUM2);              p += 2;
        memcpy(p, RAP_PARAM_DESC, len_pd);             p += len_pd;
        memcpy(p, RAP_DATA_DESC, len_dd);              p += len_dd;
        SetWord16(p, RAP_LEVEL);                       p += 2;
        SetWord16(p, RAP_DATA_MAX);                    p += 2;
        SetWord32(p, (int) SV_TYPE_ALL);               p += 4;
        memcpy(p, wg, len_wg);

        TransRAP((trans_buf_t) parm, (trans_buf_t) data,
                 svr->vc, svr->send, svr->receive);

        p = ((trans_buf_t) parm)->buf;
        status = Word16(p);
        count  = Word16(p + 4);     /* p + 2 is the string converter, which
                                       only the comments would need */
        total  = Word16(p + 6);

        /*
            0 is success and 234 is "there is more where that came from",
            which is not a failure: what arrived is still a list.
        */
        if(status != 0 && status != 234)
            Error("The master browser %s answered %d to a request for the "
                  "server list", (char *) master, status);

        take_entries((found_t *) &head,
                     (unsigned char *) ((trans_buf_t) data)->buf,
                     ((trans_buf_t) data)->out, count);

        if(total > count)
            VCLog("     browse list cut short: %d of %d servers", count, total);
    }
    ExceptCatch
    {
        if(svr)  SMBDropServer((smb_server_t) svr);
        if(parm) TransFreeBuf((trans_buf_t) parm);
        if(data) TransFreeBuf((trans_buf_t) data);
        if(addr) Free((char *) addr);
        Free((char *) master);
        Free((char *) wg);
        DiscoverFree(head);
        ExceptRethrow();
    }
    SMBDropServer((smb_server_t) svr);
    TransFreeBuf((trans_buf_t) parm);
    TransFreeBuf((trans_buf_t) data);
    Free((char *) addr);
    Free((char *) master);
    Free((char *) wg);
    return head;
}
