/*
 * Linux smoke test for NetWare AFP Entry ID path lookup.
 *
 * This intentionally uses ncpfs/libncp instead of a private socket path so the
 * request is sent through the same client stack that is commonly available on
 * Linux mars_nwe test hosts.
 */

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

#include <ncp/nwcalls.h>
#include <ncp/ncplib.h>
#include <ncp/kernel/ncp.h>

#ifndef NCPC_SUBFUNCTION
#define NCPC_SUBFUNCTION 0x10000
#endif
#ifndef NCPC_SFN
#define NCPC_SFN(FN, SFN) ((FN) | ((SFN) << 8) | NCPC_SUBFUNCTION)
#endif

#define AFP_GET_ENTRY_ID_FROM_PATH_NAME 0x0c
#define NWE_INVALID_NAMESPACE 0xbf
#define NWE_INVALID_PATH      0x9c
#define AFP_TEMP_DH_NONE      0xff

static void usage(const char *prog)
{
    fprintf(stderr,
        "Usage: %s [--allow-invalid-namespace] [--allow-invalid-path] "
        "[--dir-handle N] [--raw-path] [ncpfs options] PATH\n"
        "\n"
        "ncpfs options are parsed by ncp_initialize(), for example:\n"
        "  -S SERVER -U USER -P PASSWORD -n\n"
        "\n"
        "Examples:\n"
        "  %s -S MARS -U SUPERVISOR -P secret SYS:LOGIN\n"
        "  %s --allow-invalid-namespace -S MARS SYS:LOGIN\n"
        "  %s --allow-invalid-path -S MARS SYS:NO_SUCH_PATH\n",
        prog, prog, prog, prog);
}

static int parse_u8(const char *text, unsigned int *value)
{
    char *end = NULL;
    unsigned long v;

    errno = 0;
    v = strtoul(text, &end, 0);
    if (errno || !end || *end || v > 255)
        return -1;
    *value = (unsigned int)v;
    return 0;
}

static uint32_t be32_to_cpu(const uint8_t p[4])
{
    return ((uint32_t)p[0] << 24) |
           ((uint32_t)p[1] << 16) |
           ((uint32_t)p[2] << 8)  |
           p[3];
}

static int split_volume_path(const char *path, char *volume, size_t volume_size,
                             const char **relpath)
{
    const char *colon = strchr(path, ':');
    size_t len;

    if (!colon)
        return -1;

    len = (size_t)(colon - path);
    if (!len || len >= volume_size)
        return -1;

    memcpy(volume, path, len);
    volume[len] = '\0';

    *relpath = colon + 1;
    while (**relpath == '/' || **relpath == '\\')
        (*relpath)++;

    return 0;
}

static NWCCODE allocate_temp_dir_handle(NWCONN_HANDLE conn, const char *volume,
                                        unsigned int *dir_handle)
{
    char root_path[64];
    size_t volume_len = strlen(volume);
    uint8_t request[3 + sizeof(root_path)];
    uint8_t reply_buf[2];
    NW_FRAGMENT reply;
    NWCCODE err;

    if (!volume_len || volume_len + 1 >= sizeof(root_path))
        return NWE_INVALID_PATH;

    memcpy(root_path, volume, volume_len);
    root_path[volume_len] = ':';
    root_path[volume_len + 1] = '\0';

    /*
     * mars_nwe implements the old NCP 0x16/0x13 Allocate Temporary Directory
     * Handle endpoint directly. Use that server-native call for the smoke
     * test instead of NCP 87 namespace allocation: the namespace helper is
     * useful for ncpfs against NetWare, but mars_nwe currently rejects the
     * DIRBASE volume-root form before the AFP endpoint is reached.
     *
     * Request payload after the subfunction byte:
     *   source directory handle
     *   drive letter / target slot hint
     *   path length
     *   path bytes, here "VOL:"
     */
    request[0] = 0;                 /* source dir handle */
    request[1] = 0;                 /* no client drive-letter mapping needed */
    request[2] = (uint8_t)(volume_len + 1);
    memcpy(request + 3, root_path, volume_len + 1);

    memset(reply_buf, 0, sizeof(reply_buf));
    reply.fragAddr.rw = reply_buf;
    reply.fragSize = sizeof(reply_buf);

    err = NWRequestSimple(conn,
                          NCPC_SFN(0x16, 0x13),
                          request,
                          3 + volume_len + 1,
                          &reply);
    if (err)
        return err;

    if (reply.fragSize < 1)
        return NWE_INVALID_NCP_PACKET_LENGTH;

    *dir_handle = reply_buf[0];
    return 0;
}

static void deallocate_dir_handle(NWCONN_HANDLE conn, unsigned int dir_handle)
{
    uint8_t rq[1];

    if (dir_handle == AFP_TEMP_DH_NONE)
        return;

    rq[0] = (uint8_t)dir_handle;
    (void)NWRequestSimple(conn, NCPC_SFN(0x16, 0x14), rq, sizeof(rq), NULL);
}

int main(int argc, char **argv)
{
    NWCONN_HANDLE conn;
    NW_FRAGMENT reply;
    long init_err = 0;
    const char *path = NULL;
    const char *request_path = NULL;
    unsigned int dir_handle = 0;
    unsigned int allocated_dir_handle = AFP_TEMP_DH_NONE;
    int allow_invalid_namespace = 0;
    int allow_invalid_path = 0;
    int raw_path = 0;
    int i;
    size_t path_len;
    char volume[32];
    uint8_t request[1 + 1 + 255];
    uint8_t reply_buf[4];
    NWCCODE err;

    if (NWCallsInit(NULL, NULL)) {
        fprintf(stderr, "NWCallsInit failed\n");
        return 2;
    }

    /*
     * The smoke test needs normal file-service path access before it can send
     * AFP 0x0c: it has to allocate/use a directory handle for the supplied
     * VOL:PATH.  ncp_initialize(..., 0, ...) intentionally clears the user
     * name even if -U/-P were supplied, so mars_nwe treats the connection as
     * unauthenticated and rejects non-login paths before the AFP request is
     * reached.  Require a login here; no-password users can still use -n.
     */
    conn = ncp_initialize(&argc, argv, 1, &init_err);
    if (!conn) {
        fprintf(stderr, "ncp_initialize/login failed: %ld\n", init_err);
        usage(argv[0]);
        return 2;
    }

    for (i = 1; i < argc; i++) {
        if (!strcmp(argv[i], "--allow-invalid-namespace")) {
            allow_invalid_namespace = 1;
        } else if (!strcmp(argv[i], "--allow-invalid-path")) {
            allow_invalid_path = 1;
        } else if (!strcmp(argv[i], "--raw-path")) {
            raw_path = 1;
        } else if (!strcmp(argv[i], "--dir-handle")) {
            if (++i >= argc || parse_u8(argv[i], &dir_handle)) {
                fprintf(stderr, "invalid --dir-handle value\n");
                ncp_close(conn);
                return 2;
            }
        } else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) {
            usage(argv[0]);
            ncp_close(conn);
            return 0;
        } else if (!path) {
            path = argv[i];
        } else {
            fprintf(stderr, "unexpected argument: %s\n", argv[i]);
            usage(argv[0]);
            ncp_close(conn);
            return 2;
        }
    }

    if (!path) {
        fprintf(stderr, "missing PATH\n");
        usage(argv[0]);
        ncp_close(conn);
        return 2;
    }

    request_path = path;
    if (!raw_path && dir_handle == 0 &&
        split_volume_path(path, volume, sizeof(volume), &request_path) == 0) {
        err = allocate_temp_dir_handle(conn, volume, &dir_handle);
        if (err) {
            fprintf(stderr,
                    "Allocate Temp Dir Handle failed: completion=0x%02x (%u) volume=%s path=%s\n",
                    (unsigned int)err & 0xff, (unsigned int)err, volume, path);
            ncp_close(conn);
            return 1;
        }
        allocated_dir_handle = dir_handle;
    }

    path_len = strlen(request_path);
    if (path_len > 255) {
        fprintf(stderr, "PATH is too long for AFP Get Entry ID From Path Name: %zu\n",
                path_len);
        deallocate_dir_handle(conn, allocated_dir_handle);
        ncp_close(conn);
        return 2;
    }

    request[0] = (uint8_t)dir_handle;
    request[1] = (uint8_t)path_len;
    memcpy(request + 2, request_path, path_len);

    memset(reply_buf, 0, sizeof(reply_buf));
    reply.fragAddr.rw = reply_buf;
    reply.fragSize = sizeof(reply_buf);

    err = NWRequestSimple(conn,
                          NCPC_SFN(0x23, AFP_GET_ENTRY_ID_FROM_PATH_NAME),
                          request,
                          2 + path_len,
                          &reply);

    if (((unsigned int)err & 0xff) == NWE_INVALID_NAMESPACE && allow_invalid_namespace) {
        printf("AFP Get Entry ID From Path Name returned invalid namespace "
               "as expected: path=%s request_path=%s dir_handle=%u\n",
               path, request_path, dir_handle);
        deallocate_dir_handle(conn, allocated_dir_handle);
        ncp_close(conn);
        return 0;
    }

    if (((unsigned int)err & 0xff) == NWE_INVALID_PATH && allow_invalid_path) {
        printf("AFP Get Entry ID From Path Name returned invalid path "
               "as expected: path=%s request_path=%s dir_handle=%u\n",
               path, request_path, dir_handle);
        deallocate_dir_handle(conn, allocated_dir_handle);
        ncp_close(conn);
        return 0;
    }

    if (err) {
        fprintf(stderr,
                "AFP Get Entry ID From Path Name failed: completion=0x%02x (%u) path=%s request_path=%s dir_handle=%u\n",
                (unsigned int)err & 0xff, (unsigned int)err, path,
                request_path, dir_handle);
        deallocate_dir_handle(conn, allocated_dir_handle);
        ncp_close(conn);
        return 1;
    }

    if (reply.fragSize < 4) {
        fprintf(stderr, "short AFP reply: %zu bytes\n", reply.fragSize);
        deallocate_dir_handle(conn, allocated_dir_handle);
        ncp_close(conn);
        return 1;
    }

    printf("AFP Entry ID path=%s request_path=%s dir_handle=%u entry_id=0x%08x (%u)\n",
           path, request_path, dir_handle,
           (unsigned int)be32_to_cpu(reply_buf),
           (unsigned int)be32_to_cpu(reply_buf));

    deallocate_dir_handle(conn, allocated_dir_handle);
    ncp_close(conn);
    return 0;
}
