/*
 * 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 2026 RISC OS Developments */
 
/*
 *   Copyright 1996 Warm Silence Software Ltd.  All rights reserved.
 *   Use is subject to license terms.
 */
 
/*   PHBG 7/10/96: Initial version
 */

// #include "MemCheck:MemCheck.h"
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>
#include "swis.h"
#include "LanMan98BaseLib/strext.h"
#include "LanMan98BaseLib/memory.h"
#include "LanMan98BaseLib/error.h"
#include "LanMan98BaseLib/time.h"
#include "LanMan98BaseLib/callback.h"
#include "LanMan98BaseLib/blocked.h"
#include "LanMan98BaseLib/thread.h"
#include "LanMan98FSLib/fsys_err.h"
#include "LanMan98FSLib/fspatch.h"
#include "date.h"
#include "var.h"
#include "mapping.h"
#include "logon.h"
#include "netbios.h"
#include "discover.h"
#include "smb.h"
#include "smb2.h"
#include "webdav.h"
#include "webhttp.h"
#include "webio.h"
#include "vc.h"
#include "smbsvr.h"
#include "printer.h"
#include "nodebug.h"

#include "LanMan98FSLib/fsys.h"
#include "mount.h"

/*
    A server may be named the NetBIOS way, in fifteen characters, or by its
    full name on the internet, which may run to 253.  The NetBIOS limit is
    applied where a NetBIOS name is made, not here.
*/
#define MAX_SERVER_NAME_LEN (253)
/*
    The transfer size this filing system advertises.  The layer above asks
    for one block at a time, so this, not the SMB command in use, is what
    decides how many round trips a file costs.  It was 512, which is a
    floppy disc sector and predates every part of the path it now sits on.
*/
/*
    The buffer size handed to FileSwitch when a file is opened.

    It was 4096, which is outside what FileSwitch allows: FSEntry_Open is
    documented as returning "0 if file unbuffered, else must be a power of
    2 between 64 and 1024".  RISC OS 5 does not check, so it worked there
    and nowhere else.  A 26 bit RISC OS does check, and refuses the open
    with error &412, "Bad buffer size" - after this filing system has said
    the open succeeded, so the file appeared to open and was then never
    read from and never closed, and every application reported the same
    unhelpful thing when a file was double clicked.

    1024 is the largest the specification allows.
*/
#define BLOCK_SIZE (1024)

/*
    How often, in centiseconds, an idle connection is prodded so that it
    stays up.

    A hundred seconds is comfortable on a local network, where nothing in
    the middle is counting.  It is not comfortable across anything else: a
    NAT or a stateful firewall on the path forgets an idle mapping on its
    own schedule, and the shorter ones drop it well inside this.  The
    session is then gone without either end having said so, and the next
    operation fails rather than the mount reconnecting quietly.

    LanMan98$KeepAlive overrides it.  Over a wider network something around
    3000 - half a minute - sits inside the usual mapping lifetimes; a
    keepalive is one echo and its reply, so the cost of being early is
    close to nothing and the cost of being late is a dropped session.
*/
#define KEEPALIVE_PERIOD (10000)

/*
    How often, in centiseconds, the directory a viewer was last opened on
    is re-read to see whether anybody else has changed it, and how long to
    wait before trying again when the filing system is busy.
    Default is 0 (disabled) because background polling causes foreground
    stalls during continuous file streaming (e.g. music/audio playback)
    and spurious Filer redraw loops on volatile server metadata.
*/
#define WATCH_PERIOD (0)
#define WATCH_BUSY_RETRY (20)

/*
    How often, in centiseconds, the network is asked what is on it, how
    long the answers are collected for, and how often that collecting is
    looked in on.  See sweep_tick().
*/
#define SWEEP_PERIOD (30000)
#define SWEEP_LISTEN (300)
#define SWEEP_TICK (25)
#define SWEEP_BUSY_RETRY (100)
/*
    How often the switch itself is looked at while the sweep is off.  It
    has to be much shorter than the sweep period or turning the sweep back
    on would appear to do nothing for minutes; all it costs meanwhile is
    reading one variable.
*/
#define SWEEP_OFF_RETRY (1000)

/*
    How often, at most, a lookup of the root checks that the connection is
    still there.  See fsys_path2object().
*/
#define ROOT_CHECK_PERIOD (500)

struct fsys_image_s
{
    smb_server_t smb_server;
    thread_t keepalive;
    int store_file_types;
    int respect_hidden;
    int bad_reconnect;
    int reconnect_time;
    int root_checked;           /* when a root lookup last proved the circuit */
    /* Watching a directory for changes made elsewhere - see dirwatch(). */
    thread_t watch;
    void *watch_ctx;
    int watch_abort;
    char *watch_name;
    char *watch_special;
    unsigned int watch_sig;
    int watch_known;
    unsigned int watch_pending_sig;
    int watch_pending;
    int watch_due;              /* WebDAV: period elapsed, scan not yet run */
    int watch_next;             /* earliest time for the next WebDAV scan */
};

struct fsys_object_s
{
    smb_obj_t smb_obj;
    int refs;
    int dirty;
    int ro_type;
    char *ro_name;
    fsys_image_t img;
};

struct fsys_handle_s
{
    int is_printer;
    int fd;
    smb_handle_t smb_handle;
    fsys_object_t obj;
};

char *FsysFSName = "LanMan98";

int   FsysFSNumber = 156;

//int   FsysFlags = (FSYS_FLAG_SPECIAL_FIELDS | FSYS_FLAG_IMAGE_FILES | FSYS_FLAG_NULL_PATHS);
int   FsysFlags = (int)(FSYS_FLAG_SPECIAL_FIELDS | FSYS_FLAG_NULL_PATHS);

char *FsysStartText = "LanMan98 starting";

fsys_image_t FsysImageFromFileHandle(int x)
{
    Error("Not an image filing system");
    return NULL;
}


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

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

void FsysShut(void)
{
    mount_t mt;

    FsysSweepStop();
    PrinterCloseAll();
    while((mt = FsysNthMount(0)) != NULL)
        FsysDismount(mt);
}



struct mount_s
{
    int full_address;
    tcp_port_t port;        /* as written in the mount, 0 if not given */
    tcp_port_t live_port;   /* as actually connected, for reconnection */
    char *name;
    char *addr;
    char *server;
    char *path;
    char *share;
    char *user;
    char *passwd;
    fsys_image_t img;
    int keepalive_cb;       /* a keepalive callback chain is already ticking */
    mount_t link;
};

static fsys_image_t user_of_file_descriptor[256] = {NULL};

static void close_all_open_files(fsys_image_t img)
{
    int i;

    for(i = 1; i <256; i++)
        if(user_of_file_descriptor[i] == img)
            _swix(OS_Find, _IN(0)|_IN(1), 0, i);
}

static mount_t mounts = NULL;

static mount_t mount(void)
{
    mount_t ml;

    ml = Malloc(sizeof(*ml));
    memset(ml, 0, sizeof(*ml));
    return ml;
}

static void mount_destruct(mount_t ml)
{
    static char buf[80];

    sprintf(buf, "Filer_CloseDir LanMan98::%s.$", ml->name);
    _kernel_oscli(buf);
    sprintf(buf, "Filer_CloseDir LanMan98#notypes::%s.$", ml->name);
    _kernel_oscli(buf);
    if(ml->name) Free(ml->name);
    if(ml->addr) Free(ml->addr);
    if(ml->server) Free(ml->server);
    if(ml->path) Free(ml->path);
    if(ml->share) Free(ml->share);
    if(ml->user) Free(ml->user);
    if(ml->passwd) Free(ml->passwd);
    if(ml->img)
    {
        close_all_open_files(ml->img);
        FsPatchDropCaches();
        FsysKillImage(ml->img);
    }
    Free(ml);
}

static void check_not_already_mounted(char *server, char *path)
{
    mount_t m;

    for(m = mounts; m; m = m->link)
        if(ci_strcmp(m->server, server) == 0 && ci_strcmp(m->path, path) == 0)
            Error("Already connected to this share on this server");
}


fsys_image_t FsysImageFromDiscName(char *disc, char *special)
{
    if(disc)
    {
        mount_t m;

        for(m = mounts; m; m = m->link)
        {
            if(ci_strcmp(m->name, disc) == 0)
            {
                if(special == NULL || *special == 0)
                    m->img->store_file_types = 1;
                else if(ci_strcmp(special, "notypes") == 0)
                    m->img->store_file_types = 0;
                else
                    ErrorNum(ERR_DiscNotFound);
                return m->img;
            }
        }
        ErrorNum(ERR_DiscNotFound);
        return NULL;
    }
    else
    {
        return PrinterImageFromSpecialField(special);
    }
}

static int in_use = 0;

static int keepalive_period(void)
{
    char *var;
    int period;

    period = KEEPALIVE_PERIOD;
    var = VarRead("LanMan98$KeepAlive");
    if(var)
    {
        period = atoi(var);
        Free(var);
    }
    /*
        Nonsense, or an attempt to switch it off, falls back to the
        default: a connection with nothing holding it up is dropped by the
        server, and that is not a state worth letting anybody configure.
    */
    return (period > 0) ? period : KEEPALIVE_PERIOD;
}

static void keepalive(mount_t mt)
{
    mount_t m;
    fsys_image_t img;
    static int id = 0;

    ExceptTry
    {
        mt->keepalive_cb = 0;           /* this one has fired */
        if(in_use)
        {
            CallBackAfter((void (*)(void *))keepalive, mt, keepalive_period());
            mt->keepalive_cb = 1;
        }
        else
        {
            img = NULL;
            for(m = mounts; m; m = m->link)
                if(mt == m)
                    img = m->img;
            if(img)
            {
                if(img->keepalive == NULL)
                {
                    img->keepalive = Thread((void (*)(void *))SMBKeepAlive, img->smb_server);
                }
                if(ThreadRipe(img->keepalive))
                {
                    ThreadService(img->keepalive);
                }
                if(ThreadHasCompleted(img->keepalive))
                {
                    ExceptTry
                    {
                        ThreadThrowError(img->keepalive);
                    }
                    ExceptCatch
                    {
                        int errnum;

                        errnum = ExceptCaught()->errnum;
                        if(errnum != 0x10002 && errnum != 0x10003)
                        {
                            SMBFlushLine(img->smb_server);
                        }
                    }
                    ThreadDestruct(img->keepalive);
                    img->keepalive = NULL;
                }
                CallBackAfter((void (*)(void *))keepalive, mt, img->keepalive == NULL
                                                                   ? keepalive_period()
                                                                   : 1);
                mt->keepalive_cb = 1;
            }
        }
    } ExceptCatch {show_error(&id, "KeepAlive", ExceptCaught());}
}

/*
    Watching a directory for changes made by somebody else.

    FileSwitch raises UpCall_ModifyingFile for every change this machine
    makes, so a Filer viewer follows those without any help from here.
    Nothing announces a change made by another client on the same server:
    the file is simply not there until something re-reads the directory.

    There is no notification to listen for on the wire either.  SMB2 has
    CHANGE_NOTIFY, but it is an asynchronous request - the server answers
    STATUS_PENDING and replies properly much later - and every request this
    client makes is synchronous.  So the directory most recently listed on
    each mount is re-listed on a timer and compared with what it held last
    time, and a difference raises the same UpCall FileSwitch would have
    raised, which the Filer cannot tell apart from a local change.

    The scan runs as a thread serviced from a callback, exactly as the
    keepalive does, and shares the one connection with it.  Anything in the
    foreground must therefore drain it first: finish_keepalive does that
    for both, so every existing call site is already covered.  Draining
    costs at most one round trip rather than a whole enumeration, because
    the scan tests watch_abort between entries.

    The context block belongs to this code, not to the thread, because the
    result has to outlive the thread that computed it; watch_scan must not
    free it.

    LanMan98$WatchPeriod is the interval in centiseconds.  Setting it to 0
    switches the watch off.
*/

struct watch_ctx_s
{
    fsys_image_t img;
    path_t path;
    unsigned int sig;
    int complete;
};

static int watch_period(void)
{
    char *var;
    int period;

    period = WATCH_PERIOD;
    var = VarRead("LanMan98$WatchPeriod");
    if(var)
    {
        period = atoi(var);
        Free(var);
    }
    return period;
}

static void watch_scan(struct watch_ctx_s *ctx)
{
    smb_obj_t obj;
    unsigned int sig, entry, value;
    char *name;

    ExceptTry
    {
        sig = 0;
        obj = ctx->path->ntok > 1
                  ? SMBPath2Obj(ctx->path, ctx->img->smb_server, "")
                  : SMBRoot(ctx->img->smb_server);
        if(obj)
        {
            obj = SMBIn(obj);
            while(obj && !ctx->img->watch_abort)
            {
                name = SMBGetName(obj);
                /*
                    "/" and "//" are how the two dot entries arrive; they
                    are not shown and their stamps move for reasons that
                    are not a change to the listing.
                */
                if(strcmp(name, "/") != 0 && strcmp(name, "//") != 0)
                {
                    /* Servers need not return a directory in a stable order.
                       Hash each entry separately and add the results, making
                       the catalogue signature independent of enumeration
                       order.  The old rolling hash changed when only the
                       order changed, forcing a needless Filer reread and
                       foreground hourglass every watch period. */
                    entry = 2166136261u;
                    while(*name)
                    {
                        entry ^= (unsigned char) *name++;
                        entry *= 16777619u;
                    }
                    value = (unsigned int) SMBGetLength(obj);
                    entry = (entry ^ (value & 255u)) * 16777619u;
                    entry = (entry ^ ((value >> 8) & 255u)) * 16777619u;
                    entry = (entry ^ ((value >> 16) & 255u)) * 16777619u;
                    entry = (entry ^ ((value >> 24) & 255u)) * 16777619u;
                    value = (unsigned int) SMBGetAttributes(obj);
                    entry = (entry ^ (value & 255u)) * 16777619u;
                    entry = (entry ^ ((value >> 8) & 255u)) * 16777619u;
                    entry = (entry ^ ((value >> 16) & 255u)) * 16777619u;
                    entry = (entry ^ ((value >> 24) & 255u)) * 16777619u;
                    sig += entry;
                }
                obj = SMBNext(obj);
            }
            if(obj)
            {
                SMBObjDestruct(obj);    /* abandoned part way */
            }
            else
            {
                ctx->sig = sig;
                ctx->complete = 1;
            }
        }
    }
    ExceptCatch
    {
        /*
            A directory that has been removed, or a connection that has
            gone, is not this code's business to report - whatever the user
            does next will raise it properly.
        */
        ThreadOkay();
        ctx->complete = 0;
    }
}

static void watch_tell_filer(fsys_image_t img)
{
    _kernel_swi_regs regs;

    /*
        Reason 6 with the directory's own name is the one route through the
        Filer's handler that dirties the directory itself rather than its
        parent alone: it marks the parent, then the object and its children.
    */
    regs.r[0] = 3;                              /* UpCall_ModifyingFile */
    regs.r[1] = (int) img->watch_name;
    regs.r[2] = (int) "";
    regs.r[6] = (int) (img->watch_special ? img->watch_special : "");
    regs.r[7] = (int) "";
    regs.r[8] = FsysFSNumber;
    regs.r[9] = 6;                              /* upfsfile_Delete */
    _kernel_swi(OS_UpCall, &regs, &regs);
}

/*
    Take a finished scan apart.  may_tell is 0 when this is called from
    inside a filing system operation, where raising an UpCall could re-enter
    this filing system before it has finished what it is doing.
*/
static void watch_reap(fsys_image_t img, int may_tell)
{
    struct watch_ctx_s *ctx;

    if(img->watch == NULL)
        return;
    ExceptTry
    {
        ThreadThrowError(img->watch);
    }
    ExceptCatch {}
    ThreadDestruct(img->watch);
    img->watch = NULL;
    ctx = (struct watch_ctx_s *) img->watch_ctx;
    img->watch_ctx = NULL;
    if(ctx)
    {
        if(ctx->complete)
        {
            if(!img->watch_known)
            {
                /* The Filer has just supplied this directory.  Use the first
                   background scan as its baseline without immediately making
                   the Filer read the same directory again. */
                img->watch_sig = ctx->sig;
                img->watch_known = 1;
                img->watch_pending = 0;
            }
            else if(ctx->sig == img->watch_sig)
            {
                img->watch_pending = 0;
            }
            else if(img->watch_pending
                    && ctx->sig == img->watch_pending_sig)
            {
                /* Refresh only after the same change survives two scans.
                   Some SMB servers vary volatile catalogue metadata between
                   enumerations; immediately reporting every different value
                   creates an endless foreground Filer/hourglass loop. */
                if(may_tell)
                    watch_tell_filer(img);
                img->watch_sig = ctx->sig;
                img->watch_pending = 0;
            }
            else
            {
                img->watch_pending_sig = ctx->sig;
                img->watch_pending = 1;
            }
        }
        if(ctx->path) FsPatchPathFree(ctx->path);
        Free(ctx);
    }
}

static void finish_dirwatch(fsys_image_t img)
{
    if(img->watch)
    {
        img->watch_abort = 1;
        ThreadDestruct(img->watch);
        img->watch = NULL;
        if(img->watch_ctx)
        {
            struct watch_ctx_s *ctx = (struct watch_ctx_s *)img->watch_ctx;
            img->watch_ctx = NULL;
            if(ctx->path) FsPatchPathFree(ctx->path);
            Free(ctx);
        }
    }
}

static void watch_start(fsys_image_t img)
{
    struct watch_ctx_s *volatile ctx;

    ctx = Malloc(sizeof(*ctx));
    ctx->img = img;
    ctx->path = NULL;
    ctx->sig = 0;
    ctx->complete = 0;
    ExceptTry
    {
        ctx->path = FsPatchName2Path(img->watch_name, img);
    }
    ExceptCatch
    {
        Free(ctx);
        ctx = NULL;
    }
    if(ctx)
    {
        img->watch_abort = 0;
        img->watch_ctx = ctx;
        img->watch = Thread((void (*)(void *))watch_scan, ctx);
    }
}

static void dirwatch(mount_t mt)
{
    mount_t m;
    fsys_image_t img;
    int period;

    ExceptTry
    {
        if(in_use)
        {
            CallBackAfter((void (*)(void *))dirwatch, mt, WATCH_BUSY_RETRY);
        }
        else
        {
            img = NULL;
            for(m = mounts; m; m = m->link)
                if(mt == m)
                    img = m->img;
            if(img)
            {
                period = watch_period();
                if(img->watch)
                {
                    if(ThreadRipe(img->watch))
                        ThreadService(img->watch);
                    if(ThreadHasCompleted(img->watch))
                        watch_reap(img, 1);
                }
                /* A WebDAV re-list blocks until the server answers. Doing
                   that from this callback, or from the end of an unrelated
                   filing-system call, freezes the Filer. SMB watches stay. */
                else if(period > 0 && img->watch_name && img->smb_server
                                                      && !SMBIsWebDAV(img->smb_server)
                                                      && img->keepalive == NULL)
                {
                    watch_start(img);
                }
                CallBackAfter((void (*)(void *))dirwatch, mt,
                              img->watch ? 1
                                         : (period > 0 ? period
                                                       : keepalive_period()));
            }
        }
    }
    ExceptCatch
    {
        /* Best effort; a watch that fails must never disturb the desktop. */
    }
}

/*
    Noticing a server that was not there before.

    *Discover98 asks the network what is on it, and everything that shares
    files answers.  That is a thing the user has to think to do, so a
    machine switched on after the desktop started is not seen until they
    do it.  The same question asked on a timer notices by itself.

    The question is one UDP packet to a multicast address and the answers
    come back unbidden, so a sweep costs no connection, no credentials and
    nothing at the other end - which is why it is safe to leave running.
    Enumerating the shares each server offers is not: that needs a
    connection and a logon per server, and repeating that against every
    machine on the network every few minutes is how account lockouts
    happen.  So the sweep reports servers, and asking what any of them
    shares stays where it was, on the user's own *Discover98.

    Nothing here waits.  DiscoverBegin asks and returns, and each callback
    afterwards takes whatever has arrived until the collecting period is
    up; the machine is never held while a sweep is in progress.

    What is new is put in LanMan98$New, a comma separated list of names,
    for the Filer to show and then unset.  A variable rather than a poll
    word because the Filer already wakes twice a second on its own, and
    because it can be read from the command line, which is the only way
    any of this can be watched on a machine with no desktop.

    LanMan98$SweepPeriod is the interval in centiseconds.  Setting it to 0
    switches the sweep off.
*/

typedef struct seen_s
{
    struct seen_s *next;
    char *name;
} *seen_t;

static int sweep_hold;          /* foreground code is using the network */
static seen_t seen;             /* every server this machine has been told about */
static int seen_known;          /* whether that list is worth comparing against */
static sweep_t sweep_run;       /* the sweep in progress, if there is one */
static int sweep_going;         /* whether a callback is outstanding */

static int sweep_period(void)
{
    char *var;
    int period;

    /*
        On by default, at SWEEP_PERIOD.  It was held off while the fault
        that hung the desktop was outstanding; that turned out to be an
        unbounded loop in DiscoverPoll rather than anything about asking the
        network on a timer, and with the loop bounded this has been run at
        fifty times this rate against a real network without the desktop
        missing a redraw.  Setting the variable to 0 still switches it off.
    */
    period = SWEEP_PERIOD;
    var = VarRead("LanMan98$SweepPeriod");
    if(var)
    {
        period = atoi(var);
        Free(var);
    }
    return period;
}

/*
    Where the list of servers already announced is kept between runs.

    Without this every server on the network is new again each time the
    module is loaded, so the first sweep after a reboot would announce the
    lot.  NULL when there is nowhere to write, in which case the list is
    still kept in memory and only the memory of previous runs is lost.
*/
static char *seen_file(void)
{
    static char path[256];
    char *var;

    var = VarRead("LanMan98$SeenFile");
    if(var)
    {
        if(strlen(var) < sizeof(path))
        {
            strcpy(path, var);
            Free(var);
            return path;
        }
        Free(var);
        return NULL;
    }
    var = getenv("Choices$Write");
    if(var == NULL || *var == 0 || strlen(var) + 14 > sizeof(path))
        return NULL;
    sprintf(path, "%s.LanMan98Seen", var);
    return path;
}

static int seen_has(const char *name)
{
    seen_t s;

    for(s = seen; s; s = s->next)
        if(ci_strcmp(s->name, (char *) name) == 0)
            return 1;
    return 0;
}

static void seen_add(const char *name)
{
    seen_t s;

    s = Malloc(sizeof(*s));
    s->name = strdup((char *) name);
    s->next = seen;
    seen = s;
}

static void seen_forget(void)
{
    seen_t s, next;

    for(s = seen; s; s = next)
    {
        next = s->next;
        Free(s->name);
        Free(s);
    }
    seen = NULL;
    seen_known = 0;
}

static void seen_load(void)
{
    char line[128];
    char *path;
    FILE *fp;
    int n;

    path = seen_file();
    if(path == NULL)
        return;
    fp = fopen(path, "r");
    if(fp == NULL)
        return;
    while(fgets(line, sizeof(line), fp))
    {
        n = (int) strlen(line);
        while(n > 0 && (line[n - 1] == '\n' || line[n - 1] == '\r'))
            line[--n] = 0;
        if(n > 0 && !seen_has(line))
            seen_add(line);
    }
    fclose(fp);
    /*
        A file that was there at all means these were announced on some
        previous run, so what is not in it now is genuinely new.
    */
    seen_known = 1;
}

static void seen_save(void)
{
    char *path;
    seen_t s;
    FILE *fp;

    path = seen_file();
    if(path == NULL)
        return;
    fp = fopen(path, "w");
    if(fp == NULL)
        return;                 /* nowhere to write is not worth an error */
    for(s = seen; s; s = s->next)
        fprintf(fp, "%s\n", s->name);
    fclose(fp);
}

/*
    Work out what in a finished sweep had not been seen before, remember
    it, and say so.  The first sweep of all only remembers: announcing
    every machine that was already there is not news.
*/
static void sweep_announce(found_t list)
{
    char names[256];
    found_t f;
    int len, n, any;

    names[0] = 0;
    len = 0;
    n = 0;
    any = 0;
    for(f = list; f; f = f->next)
    {
        if(f->name == NULL || *f->name == 0 || seen_has(f->name))
            continue;
        seen_add(f->name);
        any = 1;
        if(!seen_known)
            continue;
        if(len + (int) strlen(f->name) + 2 < (int) sizeof(names))
        {
            if(n++)
                names[len++] = ',';
            strcpy(names + len, f->name);
            len += (int) strlen(f->name);
        }
    }
    if(n)
        VarSet("LanMan98$New", names);
    if(any)
        seen_save();
    seen_known = 1;
}

/*
    Which sweep chain is the live one.

    Stopping the sweep cannot cancel the callback that is already pending,
    so a stop followed by a start used to leave two chains ticking, each
    asking the network what is on it.  Every chain carries the generation
    it was started in, and a chain whose generation has passed simply ends.
*/
static int sweep_gen = 0;

static void sweep_tick(void *gen)
{
    int next;
    int period;

    if((int) gen != sweep_gen)
        return;                 /* an older chain: let it end here */
    period = SWEEP_PERIOD;
    next = SWEEP_TICK;
    ExceptTry
    {
        period = sweep_period();
        if(period <= 0)
        {
            /*
                Switched off.  Anything part way through is dropped rather
                than finished, and the chain keeps ticking slowly so that
                switching it back on takes effect without a reload.
            */
            if(sweep_run)
            {
                DiscoverAbandon(sweep_run);
                sweep_run = NULL;
            }
            next = SWEEP_OFF_RETRY;
        }
        else if(in_use || sweep_hold)
        {
            next = SWEEP_BUSY_RETRY;
        }
        else if(sweep_run)
        {
            if(DiscoverPoll(sweep_run))
            {
                next = SWEEP_TICK;
            }
            else
            {
                found_t list;

                list = DiscoverEnd(sweep_run);
                sweep_run = NULL;
                sweep_announce(list);
                DiscoverFree(list);
                next = period;
            }
        }
        else
        {
            sweep_run = DiscoverBegin(SWEEP_LISTEN);
            next = SWEEP_TICK;
        }
    }
    ExceptCatch
    {
        /*
            A network that will not carry the question, or no network at
            all, is the ordinary case on a machine that is not plugged in.
            Drop whatever was in progress and try again later; a sweep that
            fails must never disturb the desktop.
        */
        if(sweep_run)
        {
            DiscoverAbandon(sweep_run);
            sweep_run = NULL;
        }
        next = period > 0 ? period : SWEEP_OFF_RETRY;
    }
    ExceptTry
    {
        CallBackAfter(sweep_tick, gen, next);
    }
    ExceptCatch
    {
        sweep_going = 0;        /* the chain is broken; FsysSweepStart restarts it */
    }
}

/*
    Keep the sweep out of the way while the foreground is using the network.

    The callback that drives the sweep fires at any safe point, including
    inside a *command that is part way through talking to a server - the
    filing system's own in_use flag does not cover those, because a command
    is not an entry to the filing system.  So a second socket could be
    opened, and read from, underneath one that was already in use.

    Anything the sweep has part way through is dropped rather than left
    open alongside the foreground's own: a sweep is due again in minutes
    and nothing is lost by abandoning this one.
*/
void FsysSweepHold(int on)
{
    if(on)
    {
        sweep_hold++;
        if(sweep_run)
        {
            DiscoverAbandon(sweep_run);
            sweep_run = NULL;
        }
    }
    else if(sweep_hold > 0)
    {
        sweep_hold--;
    }
}

void FsysSweepStart(void)
{
    if(sweep_going)
        return;
    sweep_going = 1;
    sweep_gen++;
    seen_load();
    ExceptTry
    {
        CallBackAfter(sweep_tick, (void *) sweep_gen, SWEEP_LISTEN);
    }
    ExceptCatch
    {
        sweep_going = 0;
    }
}

void FsysSweepStop(void)
{
    if(sweep_run)
    {
        DiscoverAbandon(sweep_run);
        sweep_run = NULL;
    }
    seen_forget();
    sweep_going = 0;
    sweep_gen++;                /* the pending chain, if any, ends itself */
}


void FsysDirRead(fsys_image_t img, char *name, char *special)
{
    if(img == NULL || name == NULL || PrinterClaimsImage(img))
        return;
    if(img->watch_name
        && strcmp(img->watch_name, name) == 0
        && (img->watch_special == NULL) == (special == NULL)
        && (special == NULL || strcmp(img->watch_special, special) == 0))
        return;                 /* the same directory: keep its signature */

    if(img->watch_name) Free(img->watch_name);
    if(img->watch_special) Free(img->watch_special);
    img->watch_name = NULL;
    img->watch_special = NULL;
    img->watch_known = 0;       /* the first scan only records a baseline */
    img->watch_pending = 0;
    img->watch_due = 0;
    img->watch_next = Time() + watch_period();
    ExceptTry
    {
        img->watch_name = strdup(name);
        if(special) img->watch_special = strdup(special);
    }
    ExceptCatch
    {
        if(img->watch_name) Free(img->watch_name);
        img->watch_name = NULL;
        img->watch_special = NULL;
    }
}

static void finish_keepalive(fsys_image_t img)
{
    PrinterImageRefuse(img, 1);
    finish_dirwatch(img);
    if(img->keepalive)
    {
        while(!ThreadHasCompleted(img->keepalive))
        {
            BlockedWait();
            if(ThreadRipe(img->keepalive))
            {
                ThreadService(img->keepalive);
            }
        }
        ExceptTry
        {
            ThreadThrowError(img->keepalive);
        }
        ExceptCatch
        {
            int errnum;

            errnum = ExceptCaught()->errnum;
            if(errnum != 0x10002 && errnum != 0x10003)
            {
                SMBFlushLine(img->smb_server);
            }
        }
        ThreadDestruct(img->keepalive);
        img->keepalive = NULL;
    }
}

/*
    Names recently looked for and not found, and how long that is believed.

    Creating a file costs FileSwitch and the program doing it several
    lookups of the same name, each a round trip or two - the plain name,
    then the name with a type on it - and every one of them asks about a
    name that is not there yet.  A name that was not there a moment ago is
    taken still not to be there for two seconds (Windows' own client does
    the same for five), unless this client has changed something on the
    share since, which forgets the lot.  A name found only with a type on
    it is remembered as such, so that its plain lookup is not repeated
    either.

    LanMan98$NotFoundCache sets the time in centiseconds; 0 turns it off.
*/
#define NOTFOUND_TIME   (200)
#define NOTFOUND_SLOTS  (16)
#define MISS_NONE       (0)
#define MISS_ALL        (1)     /* neither the name nor name,xxx */
#define MISS_PLAIN      (2)     /* not the name, but name,xxx */

typedef struct
{
    fsys_image_t img;
    int when;
    int kind;
    char key[232];
} notfound_t;

static notfound_t notfound[NOTFOUND_SLOTS];
static int notfound_next = 0;

static int notfound_time(void)
{
    char *var = getenv("LanMan98$NotFoundCache");

    return var ? atoi(var) : NOTFOUND_TIME;
}

/* The whole path as one string, or 0 if it will not fit */
static int notfound_key(path_t path, char *key, int max)
{
    int i, n, len;

    n = 0;
    for(i = 0; i < path->ntok; i++)
    {
        len = strlen(path->tok[i]);
        if(n + len + 2 > max)
            return 0;
        key[n++] = '.';
        memcpy(key + n, path->tok[i], len);
        n += len;
    }
    key[n] = 0;
    return 1;
}

static int notfound_find(fsys_image_t img, char *key)
{
    int i, ttl;

    if(SMBIsWebDAV(img->smb_server)) return MISS_NONE;
    ttl = notfound_time();
    if(ttl <= 0)
        return MISS_NONE;
    for(i = 0; i < NOTFOUND_SLOTS; i++)
        if(notfound[i].kind && (notfound[i].img == img) &&
           (Time() - notfound[i].when < ttl) &&
           (ci_strcmp(notfound[i].key, key) == 0))
            return notfound[i].kind;
    return MISS_NONE;
}

static void notfound_note(fsys_image_t img, char *key, int kind)
{
    int i;

    if(SMBIsWebDAV(img->smb_server) || notfound_time() <= 0)
        return;
    for(i = 0; i < NOTFOUND_SLOTS; i++)
        if(notfound[i].kind && (notfound[i].img == img) &&
           (ci_strcmp(notfound[i].key, key) == 0))
            break;
    if(i == NOTFOUND_SLOTS)
    {
        i = notfound_next;
        notfound_next = (notfound_next + 1) % NOTFOUND_SLOTS;
    }
    notfound[i].img = img;
    notfound[i].when = Time();
    notfound[i].kind = kind;
    strcpy(notfound[i].key, key);
}

/* Something on this share changed, or the share is going: forget it all */
static void notfound_forget(fsys_image_t img)
{
    int i;

    for(i = 0; i < NOTFOUND_SLOTS; i++)
        if((img == NULL) || (notfound[i].img == img))
            notfound[i].kind = MISS_NONE;
}

static void reconnect(fsys_image_t img)
{
    mount_t mt;

    if(img)
    {
        for(mt = mounts; mt && mt->img != img; mt = mt->link)
            ;
        if(mt)
        {
            close_all_open_files(mt->img);
            FsPatchDropCaches();
            notfound_forget(mt->img);
            finish_dirwatch(mt->img);
            mt->img->watch_known = 0;   /* the old signature means nothing
                                           against a new connection */
            if(mt->img->keepalive)
            {
                ThreadDestruct(mt->img->keepalive);
                mt->img->keepalive = NULL;
            }
            if(mt->img->smb_server)
            {
                SMBReconnect(mt->img->smb_server, mt->addr,
                                                 mt->live_port,
                                                 mt->server,
                                                 mt->share,
                                                 mt->user,
                                                 mt->passwd);
            }
            else
            {
                mt->img->smb_server = SMBConnect(mt->addr,
                                                 mt->live_port,
                                                 mt->server,
                                                 mt->share,
                                                 mt->user,
                                                 mt->passwd);
            }
            mt->live_port = SMBPort(mt->img->smb_server);
            if(WebDAVAddress(mt->server)) WebDAVMounted(mt->img->smb_server);
            mt->img->bad_reconnect = 0;
            mt->img->root_checked = Time();
            /*
                A keepalive chain reschedules itself for as long as the
                mount exists, and there is no way to cancel one: callbacks
                are a table of free slots, not a queue that can be searched.
                Starting one here regardless left every reconnect with
                another chain prodding the same server, for ever.  One is
                started only if the chain is known to have stopped, which
                is what a failure inside keepalive() leaves behind.
            */
            if(!mt->keepalive_cb)
            {
                CallBackAfter((void (*)(void *))keepalive, mt, keepalive_period());
                mt->keepalive_cb = 1;
            }
        }
    }
}

static int read3hex(char *p)
{
    int i, n;

    n = 0;
    for(i = 0; i < 3; i++)
    {
        n <<= 4;
        switch(p[i])
        {
            case '0': break;
            case '1': n += 0x1; break;
            case '2': n += 0x2; break;
            case '3': n += 0x3; break;
            case '4': n += 0x4; break;
            case '5': n += 0x5; break;
            case '6': n += 0x6; break;
            case '7': n += 0x7; break;
            case '8': n += 0x8; break;
            case '9': n += 0x9; break;
            case 'a': n += 0xa; break;
            case 'b': n += 0xb; break;
            case 'c': n += 0xc; break;
            case 'd': n += 0xd; break;
            case 'e': n += 0xe; break;
            case 'f': n += 0xf; break;
            default: return -1;
        }
    }
    return p[i] == 0 ? n : -1;
}

static int name2type(char *name)
{
    int type;
    char *p;

    type = -1;
    p = strrchr(name, ',');
    if(p)
        type = read3hex(p+1);
    if(type == -1)
    {
        p = strrchr(name, '/');
        type = MapApply(p ? p+1 : NULL);
    }
    return type;
}

static char *full_name(char *name, int type)
{
    char *p, *res;

    p = strrchr(name, '/');
    if(type == MapApply(p ? p+1 : NULL))
    {
        res = strdup(name);
    }
    else
    {
        res = Malloc(strlen(name) + 5);
        sprintf(res, "%s,%03x", name, type);
    }
    return res;
}

static char *drop_ext(char *name)
{
    char *p, *res;
    int len;

    p = strrchr(name, ',');
    if(p && read3hex(p+1) != -1)
    {
        len = p-name;
        res = Malloc(len+1);
        memcpy(res, name, len);
        res[len] = 0;
    }
    else
    {
        res = strdup(name);
    }
    return res;
}

static int has_ext(char *name)
{
    char *p;

    p = strrchr(name, ',');
    return p && read3hex(p+1) != -1;
}

void FsysEnter(void)
{
    static _kernel_oserror reent_err = {0x100A0, "LanMan98 in use"};

    if(in_use)
        ErrorErr(&reent_err);
    in_use = 1;
}

void FsysLeave(void)
{
    in_use = 0;
}

mount_t FsysFindMount(char *name)
{
    mount_t m;

    for(m = mounts; m; m = m->link)
        if(ci_strcmp(m->name, name) == 0)
            return m;
    return NULL;
}

mount_t FsysNthMount(int i)
{
    mount_t m;

    for(m = mounts; m; m = m->link)
        if(i-- == 0)
            return m;
    return NULL;
}

mount_t FsysMount(char *name, char *server_addr, char *path, char *user, char *passwd)
{
    mount_t mt;
    char *addr;
    int l, watch_cs;
    char server[MAX_SERVER_NAME_LEN+1];
    int dav = WebDAVAddress(server_addr);

    // split up server name and addr
    addr = dav ? NULL : strchr(server_addr, '<');
    if(addr && addr[strlen(addr)-1] != '>')
        Error("Incorrectly specified server IP address.  Put the IP address, optionally followed by :port, in angle brackets after the NetBIOS name.");
    l = addr ? addr - server_addr : strlen(server_addr);
    /* \\server, as Windows writes it, means the same as server */
    while(!dav && l > 0 && (*server_addr == '\\' || *server_addr == '/'))
    {
        server_addr++;
        l--;
    }
    if(l == 0)
        Error("No server name given");
    if(l > MAX_SERVER_NAME_LEN)
        Error("Server names are never that long");
    memcpy(server, server_addr, l);
    server[l] = 0;
    /* WebDAV uses an explicit HTTP(S) URL. Windows' @SSL UNC notation is
       not an SMB hostname and should be entered as its HTTPS equivalent. */
    if(!dav && (strstr(server, "://") || strchr(server, '@') || strchr(server, '\\') ||
       strchr(server, '/')))
        Error("Unsupported server address %.40s. For WebDAV use "
#ifdef WEB_HTTP_ONLY
              "http://"
#else
              "https://"
#endif
              "server/collection; for SMB use the server name, optionally followed by <:port>", server);

    user = user ? user : "";
    passwd = passwd ? passwd : "";
    if(!dav && *user == 0 && *passwd == 0)
    {
        user = LoggedOnUser(server);
        passwd = LoggedOnPassword(server);
    }
    mt = FsysFindMount(name);
    if(mt) FsysDismount(mt);
    mt = mount();
    ExceptTry
    {
        mt->name = strdup(name);
        mt->server = strdup(server);
        mt->path = strdup(path);
        mt->user = strdup(user);
        mt->passwd = strdup(passwd);
        if(dav)
        {
            mt->addr = strdup(server);
            mt->full_address = 1;
        }
        else if(addr)
        {
            char *colon;

            // Remove <>s from addr while copying it
            mt->addr = strdup(addr+1);
            mt->addr[strlen(mt->addr)-1] = 0;
            // An optional :port may follow the address
            colon = strchr(mt->addr, ':');
            if(colon)
            {
                *colon = 0;
                mt->port = VCParsePort(colon+1);
            }
            if(*mt->addr)
            {
                mt->full_address = 1;
            }
            else
            {
                // <:445> names a port but leaves the address to be looked up
                Free(mt->addr);
                mt->addr = NetBIOSResolve(mt->server);
            }
        }
        else
        {
            mt->addr = NetBIOSResolve(mt->server);
        }
        check_not_already_mounted(mt->server, mt->path);
        if(dav)
        {
            mt->share = WebDAVMountURL(mt->server,mt->path);
        }
        else
        {
            mt->share = Malloc(strlen(mt->server) + strlen(mt->path) + 4);
            sprintf(mt->share, "\\\\%s\\%s", mt->server, mt->path);
        }
        mt->img = Malloc(sizeof(*mt->img));
        memset(mt->img, 0, sizeof(*mt->img));
        mt->img->store_file_types = 1;
        {
            char *hvar;

            hvar = VarRead("LanMan98$ShowHidden");
            mt->img->respect_hidden = (hvar == NULL);
            Free(hvar);
        }
        mt->img->smb_server = SMBConnect(mt->addr,
                                         mt->port,
                                         mt->server,
                                         mt->share,
                                         mt->user,
                                         mt->passwd);
        mt->live_port = SMBPort(mt->img->smb_server);
        if(dav) WebDAVMounted(mt->img->smb_server);
        CallBackAfter((void (*)(void *))keepalive, mt, keepalive_period());
        mt->keepalive_cb = 1;
        watch_cs = watch_period();
        CallBackAfter((void (*)(void *))dirwatch, mt,
                      watch_cs > 0 ? watch_cs : keepalive_period());
    }
    ExceptCatch
    {
        _kernel_oserror *caught = ExceptCaught();

        WebIssue("FsysMount failed for %s: %s", name ? name : "(null)",
                 caught && caught->errmess[0] ? caught->errmess : "no error given");
        mount_destruct(mt);
        ExceptRethrow();
    }
    mt->link = mounts;
    mounts = mt;
    LogMount(mt->server, mt->user, mt->passwd);
    return mt;
}

void FsysDismount(mount_t mt)
{
    mount_t *p;

    p = &mounts;
    while(*p)
    {
        if(*p == mt)
        {
            *p = (*p)->link;
            LogDismount(mt->server);
            mount_destruct(mt);
        }
        else
        {
            p = &(*p)->link;
        }
    }
    if(mounts == NULL)
        ThreadLibraryFinalise();
}

char *FsysMountName(mount_t mt)
{
    mount_t m;

    for(m = mounts; m; m = m->link)
        if(mt == m)
            return m->name;
    return NULL;
}

int FsysMountIsWebDAV(mount_t mt)
{
    mount_t m;

    for(m = mounts; m; m = m->link)
        if(mt == m)
            return m->img && SMBIsWebDAV(m->img->smb_server);
    return 0;
}

char *FsysMountServer(mount_t mt)
{
    mount_t m;
    static char buf[512];
    char port[12];
    int len;

    for(m = mounts; m; m = m->link)
    {
        if(mt == m)
        {
            if(m->port)
                sprintf(port, ":%d", m->port);
            else
                port[0] = 0;
            len = strlen(m->server) + strlen(port) + 3;
            if(m->full_address)
                len += strlen(m->addr);
            if((m->full_address || m->port) && len <= 512)
            {
                sprintf(buf, "%s<%s%s>", m->server,
                             m->full_address ? m->addr : "", port);
                return buf;
            }
            else
            {
                return m->server;
            }
        }
    }
    return NULL;
}

char *FsysMountPath(mount_t mt)
{
    mount_t m;

    for(m = mounts; m; m = m->link)
        if(mt == m)
            return m->path;
    return NULL;
}

char *FsysMountUser(mount_t mt)
{
    mount_t m;

    for(m = mounts; m; m = m->link)
        if(mt == m)
            return m->user;
    return NULL;
}

/*
    Append what will fit.  The status is shown in an error box, which has
    a fixed message, so a long share name is cut rather than overflowing.
*/
static void status_put(char *buf, int buflen, const char *text)
{
    int used, room, n;

    if(!buf || buflen < 2 || !text)
        return;
    used = strlen(buf);
    if(used >= buflen - 1)
        return;
    room = buflen - 1 - used;
    n = strlen(text);
    if(n > room)
        n = room;
    memcpy(buf + used, text, n);
    buf[used + n] = '\0';
}

static const char *smb1_dialect_name(int dialect)
{
    switch(dialect)
    {
        case NT:    return "SMB1 (NT LM 0.12)";
        case LM21:  return "SMB1 (LANMAN2.1)";
        case LM2:   return "SMB1 (LM1.2X002)";
        case LM1:   return "SMB1 (LANMAN1.0)";
        case MN103: return "SMB1 (MICROSOFT NETWORKS 1.03)";
        default:    return "SMB1";
    }
}

static const char *smb2_dialect_name(int dialect)
{
    switch(dialect)
    {
        case SMB2_DIALECT_0202: return "SMB 2.0.2";
        case SMB2_DIALECT_0210: return "SMB 2.1";
        case SMB2_DIALECT_0300: return "SMB 3.0";
        case SMB2_DIALECT_0302: return "SMB 3.0.2";
        case SMB2_DIALECT_0311: return "SMB 3.1.1";
        default:                return "SMB2";
    }
}

void FsysMountStatus(mount_t mt, char *buf, int buflen)
{
    mount_t m;
    smb_server_t svr;
    smb2_conn_t conn;
    int dialect;
    const char *protocol;
    const char *signing;
    const char *encryption;

    if(!buf || buflen < 2)
        Error("Status buffer is too small");
    buf[0] = '\0';
    for(m = mounts; m; m = m->link)
        if(mt == m)
            break;
    if(!m || !m->img || !m->img->smb_server)
        Error("Not connected");

    svr = m->img->smb_server;
    /* One comma-separated line: the error box this is shown in ends
       the text at the first newline, so a multi-line report would
       show only the share name.  Commas give the box's word wrapper
       clean places to break instead. */
    status_put(buf, buflen, m->name ? m->name : "");
    status_put(buf, buflen, ", ");

    if(SMBIsWebDAV(svr))
    {
#ifdef WEB_HTTP_ONLY
        status_put(buf, buflen, "WebDAV (HTTP), Signing: n/a, Encryption: none");
#else
        status_put(buf, buflen, "WebDAV (HTTPS), Signing: n/a, Encryption: TLS");
#endif
        return;
    }

    conn = (smb2_conn_t) SMBServerSMB2(svr);
    if(conn)
    {
        dialect = SMB2Dialect(conn);
        protocol = smb2_dialect_name(dialect);
        if(!SMB2Signing(conn))
            signing = "off";
        else if(dialect >= SMB2_DIALECT_0300)
            signing = "AES-CMAC";
        else
            signing = "HMAC-SHA256";
        encryption = SMB2Encrypting(conn) ? "AES-128-CCM" : "none";
    }
    else
    {
        protocol = smb1_dialect_name(SMBServerDialect(svr));
        signing = "n/a";
        encryption = "none";
    }

    status_put(buf, buflen, protocol);
    status_put(buf, buflen, ", Signing: ");
    status_put(buf, buflen, signing);
    status_put(buf, buflen, ", Encryption: ");
    status_put(buf, buflen, encryption);
}

void FsysKillImage(fsys_image_t img)
{
    PrinterImageRefuse(img, 2);
    finish_keepalive(img);
    notfound_forget(img);
    /*
        The watch has to be drained as well as the keepalive.  It was not,
        so a dismount left the thread and the block it is scanning into
        behind - and that block holds a pointer to the image about to be
        freed on the last line of this function.
    */
    finish_dirwatch(img);
    if(img->watch_name) Free(img->watch_name);
    if(img->watch_special) Free(img->watch_special);
    img->watch_name = NULL;
    img->watch_special = NULL;
    if(img->smb_server) SMBDropServer(img->smb_server);
    if(img->keepalive) ThreadDestruct(img->keepalive);
    Free(img);
}

void FsysStamp(fsys_image_t img, int now)
{
    PrinterImageRefuse(img, 3);
}

void FsysFlushImage(fsys_image_t img)
{
    PrinterImageRefuse(img, 4);
}

static void mul32x32_to_64(unsigned int x, unsigned int y, int64 *res)
{
    unsigned int ll, lh, hl, hh;

    ll = (x & 0xffff) * (y & 0xffff);
    lh = (x & 0xffff) * (y >> 16);
    hl = (x >> 16) * (y & 0xffff);
    hh = (x >> 16) * (y >> 16);
    res->high = hh + (lh >> 16) + (hl >> 16);
    hl = (hl & 0xffff) + (lh & 0xffff) + (ll >> 16);
    res->high += (hl >> 16);
    res->low = (hl << 16) | (ll & 0xffff);
}

void FsysSpace(fsys_image_t img, int64 *total, int64 *free)
{
    smb_disc_info_t dinfo;

    PrinterImageRefuse(img, 5);
    finish_keepalive(img);
    SMBDiskInfo(img->smb_server, &dinfo);
    mul32x32_to_64(dinfo.units, dinfo.free, free);
    mul32x32_to_64(dinfo.units, dinfo.total, total);
}

char *FsysGetDiscName(fsys_image_t img)
{
    PrinterImageRefuse(img, 6);
    return "";
}

void FsysSetDiscName(fsys_image_t img, char *name)
{
    PrinterImageRefuse(img, 7);
}

char *FsysDiscType(fsys_image_t img)
{
    PrinterImageRefuse(img, 8);
    return SMBIsWebDAV(img->smb_server) ? "WebDAV" : "CIFS";
}

int FsysDiscCycleId(fsys_image_t img)
{
    PrinterImageRefuse(img, 9);
    return 0;
}


char *FsysNameMap(char *name, fsys_image_t img)
{
    PrinterImageRefuse(img, 10);
    return SMBNameMap(name, img->smb_server);
}


static int next_file_type = -1;

void FsysSetCreateType(int type)
{
    next_file_type = type;
}

static fsys_object_t create_file(fsys_object_t obj, char *name, int type)
{
    fsys_object_t nobj;
    char *store_name;

    PrinterObjectRefuse(obj, 1);
    finish_keepalive(obj->img);
    notfound_forget(obj->img);
    nobj = Malloc(sizeof(*nobj));
    nobj->ro_name = NULL;
    nobj->ro_type = type >= 0 ? type : name2type(name);
    nobj->dirty = 1;
    nobj->refs = 1;
    nobj->smb_obj = NULL;
    nobj->img = obj->img;
    store_name = NULL;
    ExceptTry
    {
        store_name = type >= 0 && obj->img->store_file_types
                         ? full_name(name, type)
                         : strdup(name);
        nobj->smb_obj = SMBCreateFile(obj->smb_obj, store_name);
        Free(store_name);
        store_name = NULL;
    }
    ExceptCatch
    {
        if(store_name) Free(store_name);
        FsysFree(nobj);
        ExceptRethrow();
    }
    return nobj;
}

fsys_object_t FsysCreateFile(fsys_object_t obj, char *name)
{
    int type;

    type = next_file_type;
    next_file_type = -1;
    return create_file(obj, name, type);
}

fsys_object_t FsysCreateDirectory(fsys_object_t obj, char *name)
{
    fsys_object_t nobj;

    PrinterObjectRefuse(obj, 2);
    finish_keepalive(obj->img);
    notfound_forget(obj->img);
    nobj = Malloc(sizeof(*nobj));
    nobj->ro_name = NULL;
    nobj->ro_type = name2type(name);
    nobj->dirty = 1;
    nobj->refs = 1;
    nobj->smb_obj = NULL;
    nobj->img = obj->img;
    ExceptTry
    {
        nobj->smb_obj = SMBCreateDirectory(obj->smb_obj, name);
    }
    ExceptCatch
    {
        FsysFree(nobj);
        ExceptRethrow();
    }
    return nobj;
}

void FsysRename(fsys_object_t obj, char *name)
{
    char *ext_name;

    PrinterObjectRefuse(obj, 3);
    finish_keepalive(obj->img);
    notfound_forget(obj->img);
    if(obj->ro_name)
    {
        Free(obj->ro_name);
        obj->ro_name = NULL;
    }
    DEBUG_PRINT1("Store types: %d", obj->img->store_file_types);
    DEBUG_PRINT1("Name: %s", name);
    DEBUG_PRINT1("RO type: 0x%03x", obj->ro_type);

    ext_name = obj->img->store_file_types
                    ? full_name(name, obj->ro_type)
                    : strdup(name);
    DEBUG_PRINT1("%s", ext_name);
    ExceptTry
    {
        SMBRename(obj->smb_obj, ext_name);
    }
    ExceptCatch
    {
        Free(ext_name);
        ExceptRethrow();
    }
    Free(ext_name);
}

void FsysMove(fsys_object_t obj1, fsys_object_t obj2)
{
    PrinterObjectRefuse(obj1, 4);
    PrinterObjectRefuse(obj2, 5);
    finish_keepalive(obj1->img);
    notfound_forget(obj1->img);
    notfound_forget(obj2->img);
    SMBMove(obj1->smb_obj, obj2->smb_obj);
}

void FsysDelete(fsys_object_t obj)
{
    PrinterObjectRefuse(obj, 6);
    finish_keepalive(obj->img);
    notfound_forget(obj->img);
    SMBDelete(obj->smb_obj);
}

static int hidden(fsys_object_t obj)
{
    return obj->img->respect_hidden && (SMBGetAttributes(obj->smb_obj) & SMB_HIDDEN);
}

static int looks_like_a_dos_name(char *name)
{
    char *p;

    DEBUG_PRINT1("Name = %s", name);
    /* See if there's a ~ */
    p = strrchr(name, '~');

    /* If not, its noe a DOS name */
    if(p == NULL)
        return 0;

    /* Jump over ~ and check the rest is digits */
    for(p++; *p; p++)
        if(!isdigit(*p))
            return 0;

    return 1;
}

static void extension_search(path_t path, fsys_object_t obj)
{
    if (obj->img->smb_server->nas_basic_firmware)
    {
        /* This firmware does not support wildcards in the search other than
           a leaf of exactly "*", so we must search the entire directory */
        char *leaf = path->tok[path->ntok - 1];
        int len = strlen(leaf);

        path->tok[path->ntok - 1] = "";

        ExceptTry
        {
            obj->smb_obj = SMBPath2Obj(path, obj->img->smb_server, "*");
            while(obj->smb_obj)
            {
                char *name = SMBGetName(obj->smb_obj);
                if (has_ext(name) && (strlen(name) == (len + 4)) && (strncmp(name, leaf, len) == 0))
                    break;
                obj->smb_obj = SMBNext(obj->smb_obj);
            }
        }
        ExceptCatch
        {
            path->tok[path->ntok - 1] = leaf;
            ExceptRethrow();
        }
        path->tok[path->ntok - 1] = leaf;
    }
    else
    {
        obj->smb_obj = SMBPath2Obj(path, obj->img->smb_server, ",###");
        while(obj->smb_obj && !has_ext(SMBGetName(obj->smb_obj)))
            obj->smb_obj = SMBNext(obj->smb_obj);
    }
}

static fsys_object_t fsys_path2object(path_t path, fsys_image_t img)
{
    fsys_object_t obj;
    int errnum;
    char key[sizeof(notfound[0].key)];
    int keyed, known, plain;

    obj = Malloc(sizeof(*obj));
    obj->refs = 1;
    obj->ro_name = NULL;
    obj->smb_obj = NULL;
    obj->img = img;
    ExceptTry
    {
        if(path->ntok > 1)
        {
            if(looks_like_a_dos_name(path->tok[path->ntok-1]))
            {
               /*
                * Stupid Windows might give us the DOS name, and cause us
                * to miss the full name with ,xxx extension.  So we are
                * forced to do the slow directory enumeration first.
                */
                DEBUG_PRINT0("Looks like DOS");
                ExceptTry
                {
                    if(obj->smb_obj == NULL && obj->img->store_file_types)
                    {
                        extension_search(path, obj);
                    }
                }
                ExceptCatch
                {
                    errnum = ExceptCaught()->errnum;
                    if(errnum != 0x10002 && errnum != 0x10003)
                        ExceptRethrow();
                }
                if(obj->smb_obj == NULL)
                    obj->smb_obj = SMBPath2Obj(path, img->smb_server, "");
            }
            else
            {
               /*
                * No way is this a DOS name, so its safe to do the fast
                * direct request for the file.
                */
                DEBUG_PRINT0("Looks like non-DOS");
                keyed = notfound_key(path, key, sizeof(key));
                known = keyed ? notfound_find(img, key) : MISS_NONE;
                plain = 0;
                if(known == MISS_NONE)
                {
                    ExceptTry
                    {
                        obj->smb_obj = SMBPath2Obj(path, img->smb_server, "");
                    }
                    ExceptCatch
                    {
                        errnum = ExceptCaught()->errnum;
                        if(errnum != 0x10002 && errnum != 0x10003)
                            ExceptRethrow();
                    }
                    plain = (obj->smb_obj != NULL);
                }
                if(obj->smb_obj == NULL && known != MISS_ALL &&
                   obj->img->store_file_types)
                {
                    extension_search(path, obj);
                }
                if(keyed)
                {
                    if(obj->smb_obj == NULL)
                        notfound_note(img, key, MISS_ALL);
                    else if(!plain && known == MISS_NONE)
                        notfound_note(img, key, MISS_PLAIN);
                }
            }
        }
        else
        {
            /*
                The root is known without asking, which makes a lookup of it
                the place a dropped connection goes unnoticed: one round trip
                finds out, and FsysPath2Object reconnects if it fails.  That
                was asked on every lookup, though, and every file in the root
                has the root as its parent, so each one cost it.  Once every
                few seconds proves as much.
            */
            if((img->root_checked == 0) ||
               (Time() - img->root_checked >= ROOT_CHECK_PERIOD))
            {
                SMBKeepAlive(img->smb_server);
                img->root_checked = Time();
            }
            obj->smb_obj = SMBRoot(img->smb_server);
            obj->ro_type = 0xFFD;
            obj->dirty = 0;
        }
        if(obj->smb_obj == NULL)
        {
            FsysFree(obj);
            obj = NULL;
        }
        else
        {
            obj->ro_type = name2type(SMBGetName(obj->smb_obj));
            obj->dirty = 0;
        }
    }
    ExceptCatch
    {
        FsysFree(obj);
        obj = NULL;
        errnum = ExceptCaught()->errnum;
        if(errnum != 0x10002 && errnum != 0x10003)
        {
            // show_error(&id, "Get", ExceptCaught());
            ExceptRethrow();
        }
    }
    return obj;
}

fsys_object_t FsysPath2Object(path_t path, fsys_image_t img)
{
    fsys_object_t res;

    if(PrinterClaimsImage(img))
        return PrinterPath2Object(path, img);
    finish_keepalive(img);

    if(img->bad_reconnect && Time() - img->reconnect_time < 300)
        Error("Connection down");
    ExceptTry
    {
        res = fsys_path2object(path, img);
    }
    ExceptCatch
    {
        ExceptTry
        {
            reconnect(img);
            res = fsys_path2object(path, img);
        }
        ExceptCatch
        {
            img->bad_reconnect = 1;
            img->reconnect_time = Time();
            ExceptRethrow();
        }
    }
    img->bad_reconnect = 0;
    return res;
}

fsys_object_t FsysImgRoot(fsys_image_t img)
{
    fsys_object_t obj;

    PrinterImageRefuse(img, 11);
    finish_keepalive(img);
    obj = Malloc(sizeof(*obj));
    obj->refs = 1;
    obj->ro_name = NULL;
    obj->smb_obj = SMBRoot(img->smb_server);
    obj->ro_type = 0xFFD;
    obj->dirty = 0;
    obj->img = img;
    return obj;
}

static int dots(fsys_object_t obj)
{
    char *s;

    s = SMBGetName(obj->smb_obj);
    return strcmp(s, "/") == 0 || strcmp(s, "//") == 0;
}

fsys_object_t FsysIn(fsys_object_t obj)
{
    PrinterObjectRefuse(obj, 7);
    finish_keepalive(obj->img);
    if(obj->ro_name)
    {
        Free(obj->ro_name);
        obj->ro_name = NULL;
    }
    obj->smb_obj = SMBIn(obj->smb_obj);
    while(obj->smb_obj && (dots(obj)||hidden(obj)))
        obj->smb_obj = SMBNext(obj->smb_obj);
    if(obj->smb_obj)
    {
        obj->dirty = 0;
        obj->ro_type = name2type(SMBGetName(obj->smb_obj));
        return obj;
    }
    else
    {
        FsysFree(obj);
        return NULL;
    }
}

fsys_object_t FsysNext(fsys_object_t obj)
{
    PrinterObjectRefuse(obj, 8);
    finish_keepalive(obj->img);
    if(obj->ro_name)
    {
        Free(obj->ro_name);
        obj->ro_name = NULL;
    }
    obj->smb_obj = SMBNext(obj->smb_obj);
    while(obj->smb_obj && (dots(obj)||hidden(obj)))
        obj->smb_obj = SMBNext(obj->smb_obj);
    if(obj->smb_obj)
    {
        obj->dirty = 0;
        obj->ro_type = name2type(SMBGetName(obj->smb_obj));
        return obj;
    }
    else
    {
        FsysFree(obj);
        return NULL;
    }
}

fsys_object_t FsysDup(fsys_object_t obj)
{
    fsys_object_t nobj;

    if(PrinterClaimsObject(obj))
        return obj;
    nobj = Malloc(sizeof(*nobj));
    ExceptTry
    {
        nobj->refs = 1;
        nobj->ro_name = NULL;
        nobj->dirty = 0;
        nobj->img = obj->img;
        /* Finish name conversion before acquiring the backend copy, so
           no later allocation can strand that copy on an exception. */
        nobj->ro_type = name2type(SMBGetName(obj->smb_obj));
        nobj->smb_obj = obj->smb_obj ? SMBObjDup(obj->smb_obj) : NULL;
    }
    ExceptCatch
    {
        Free(nobj);
        ExceptRethrow();
    }
    return nobj;
}

fsys_object_t FsysNewRef(fsys_object_t obj)
{
    if(PrinterClaimsObject(obj))
        return obj;
    ++(obj->refs);
    return obj;
}

void FsysFree(fsys_object_t obj)
{
    if(PrinterClaimsObject(obj))
        return;
    finish_keepalive(obj->img);
    if(--(obj->refs) == 0)
    {
        if(obj->ro_name)
            Free(obj->ro_name);
        if(obj->smb_obj)
            SMBObjDestruct(obj->smb_obj);
        Free(obj);
    }
}



char *FsysGetName(fsys_object_t obj)
{
    PrinterObjectRefuse(obj, 9);
    finish_keepalive(obj->img);
    if(obj->ro_name == NULL)
        obj->ro_name = obj->img->store_file_types
                            ? drop_ext(SMBGetName(obj->smb_obj))
                            : strdup(SMBGetName(obj->smb_obj));
    return obj->ro_name;
}

int FsysGetLength(fsys_object_t obj)
{
    if(PrinterClaimsObject(obj))
        return 0;
    finish_keepalive(obj->img);
    return SMBGetLength(obj->smb_obj);
}

void FsysSetLength(fsys_object_t obj, int l)
{
    if(PrinterClaimsObject(obj))
        return;
    // PrinterObjectRefuse(obj, 10);
    finish_keepalive(obj->img);
    SMBSetLength(obj->smb_obj, l);
}

int FsysGetLoadAddr(fsys_object_t obj)
{
    int arc[2];

    if(PrinterClaimsObject(obj))
        return (int)0xFFFFFF00;
    finish_keepalive(obj->img);
    DateSMBtoARC(SMBGetTime(obj->smb_obj), arc);
    return 0xFFF00000 | (obj->ro_type << 8) | arc[1];
}

void FsysSetLoadAddr(fsys_object_t obj, int laddr)
{
    int arc[2];

    PrinterObjectRefuse(obj, 11);
    finish_keepalive(obj->img);
    if((laddr & 0xFFF00000) == 0xFFF00000)
    {
        DateSMBtoARC(SMBGetTime(obj->smb_obj), arc);
        arc[1] = (laddr & 0xFF);
        SMBSetTime(obj->smb_obj, DateARCtoSMB(arc));
        obj->ro_type = ((laddr >> 8) & 0xFFF);
        obj->dirty = 1;
    }
}

int FsysGetExecAddr(fsys_object_t obj)
{
    int arc[2];

    if(PrinterClaimsObject(obj))
        return 0;
    finish_keepalive(obj->img);
    DateSMBtoARC(SMBGetTime(obj->smb_obj), arc);
    return arc[0];
}

void FsysSetExecAddr(fsys_object_t obj, int eaddr)
{
    int arc[2];

    PrinterObjectRefuse(obj, 12);
    finish_keepalive(obj->img);
    DateSMBtoARC(SMBGetTime(obj->smb_obj), arc);
    arc[0] = eaddr;
    SMBSetTime(obj->smb_obj, DateARCtoSMB(arc));
}

int FsysGetAttributes(fsys_object_t obj)
{
    int attrs, dos_attrs;

    if(PrinterClaimsObject(obj))
        return FSYS_READ | FSYS_WRITE;
    finish_keepalive(obj->img);
    dos_attrs = SMBGetAttributes(obj->smb_obj);
    attrs = 3;
    if(dos_attrs & SMB_READONLY)
    {
        attrs |= 0x008;
        attrs &= ~0x002;
    }
    if(dos_attrs & SMB_HIDDEN) attrs |= 0x100;
    if(dos_attrs & SMB_SYSTEM) attrs |= 0x200;
    if(dos_attrs & SMB_ARCHIVE) attrs |= 0x400;
    return attrs;
}

void FsysSetAttributes(fsys_object_t obj, int att)
{
    int dattr;

    PrinterObjectRefuse(obj, 13);
    finish_keepalive(obj->img);
    dattr = (SMBGetAttributes(obj->smb_obj) & SMB_SUBDIR);
    if(att & 0x008) dattr |= SMB_READONLY;
    /*
    if(att & 0x100) dattr |= SMB_HIDDEN;
    if(att & 0x200) dattr |= SMB_SYSTEM;
    if(att & 0x400) dattr |= SMB_ARCHIVE;
    */
    SMBSetAttributes(obj->smb_obj, dattr);
}

int FsysGetType(fsys_object_t obj)
{
    if(PrinterClaimsObject(obj))
        return FSYS_FILE;
    finish_keepalive(obj->img);
    return SMBGetAttributes(obj->smb_obj) & SMB_SUBDIR
                 ? FSYS_DIRECTORY
                 : FSYS_FILE;
}

/*
    The buffer size FileSwitch is told to use for an open file.

    FileSwitch allocates a buffer of this size for every file that is open,
    and the size has to be a power of two.  Four kilobytes suits a network
    filing system - it is the smallest that does not make reading a byte at
    a time absurd - and is what this has always returned.

    LanMan98$BlockSize overrides it, for finding out whether a machine that
    opens a file and then does nothing with it is failing to get the buffer.
    Zero says the file is not to be buffered at all, which is slower for
    reading a byte at a time but asks FileSwitch for no memory.  Anything
    that is not a power of two, or is bigger than 64K, is ignored.
*/
int FsysGetBlockSize(fsys_object_t obj)
{
    char *var, *end;
    long n;

    var = getenv("LanMan98$BlockSize");
    if(var != NULL)
    {
        n = strtol(var, &end, 0);
        /* Invalid text must not silently select unbuffered I/O. Keep the
           wide result until its range is checked, before converting to int. */
        if(end != var && *end == 0)
        {
            if(n == 0)
                return 0;               /* explicitly unbuffered */
            /* Only what FileSwitch will accept: a power of two, 64 to 1024 */
            if((n >= 64) && (n <= 1024) && ((n & (n - 1)) == 0))
                return (int)n;
        }
    }
    return BLOCK_SIZE;
}

void FsysFlushObject(fsys_object_t obj)
{
    if(PrinterClaimsObject(obj))
        return;
    finish_keepalive(obj->img);
    if(obj->dirty && (SMBGetAttributes(obj->smb_obj) & SMB_SUBDIR) == 0)
    {
        if(obj->img->store_file_types && obj->ro_type != name2type(SMBGetName(obj->smb_obj)))
        {
            char *ext_name, *name;

            name = obj->img->store_file_types
                        ? drop_ext(SMBGetName(obj->smb_obj))
                        : strdup(SMBGetName(obj->smb_obj));
            ExceptTry
            {
                ext_name = full_name(name, obj->ro_type);
            }
            ExceptCatch
            {
                Free(name);
                ExceptRethrow();
            }
            Free(name);
            notfound_forget(obj->img);
            ExceptTry
            {
                SMBRename(obj->smb_obj, ext_name);
            }
            ExceptCatch
            {
                Free(ext_name);
                ExceptRethrow();
            }
            Free(ext_name);
        }
    }
    obj->dirty = 0;
    ExceptTry
    {
        SMBFlush(obj->smb_obj);
    }
    ExceptCatch
    {
        if(ExceptCaught()->errnum != 0x10020)
            ExceptRethrow();
    }
}



fsys_handle_t FsysOpen(fsys_object_t obj, int update, unsigned int fd)
{
    fsys_handle_t h;

    if(PrinterClaimsObject(obj))
        return PrinterOpen(obj, update, fd);
    finish_keepalive(obj->img);
    h = Malloc(sizeof(*h));
    ExceptTry
    {
        h->is_printer = 0;
        h->obj = obj;
        h->fd = fd;
        VCLog("open %s for %s", SMBGetName(obj->smb_obj),
              update ? "update" : "reading");
        h->smb_handle = SMBOpen(obj->smb_obj, update);
        /*
            Everything FileSwitch is about to be told.  It decides from
            these whether the file may be read at all, and in what sized
            pieces; a fault that never reaches the wire is a fault in one
            of these numbers, so log them rather than guess later.
        */
        VCLog("     length %d, handle %d, type %d, attributes &%02X%s%s",
              SMBGetLength(obj->smb_obj), fd, FsysGetType(obj),
              FsysGetAttributes(obj),
              (FsysGetAttributes(obj) & FSYS_READ) ? ", readable" : ", NOT READABLE",
              (FsysGetAttributes(obj) & FSYS_WRITE) ? ", writable" : "");
        VCLog("     block size %d%s", FsysGetBlockSize(obj),
              (FsysGetBlockSize(obj) == 0) ? " (unbuffered)" : "");
        if(fd < 256)
            user_of_file_descriptor[fd] = obj->img;
    }
    ExceptCatch
    {
        VCLogErr("open failed", ExceptCaught());
        Free(h);
        ExceptRethrow();
    }
    return h;
}

int FsysCancelStagedSave(fsys_handle_t h, fsys_object_t obj)
{
    if(!FsysIsWebDAV(obj))
        return 0;
    if(h)
    {
        SMBCancelStagedSave(h->smb_handle);
        if(h->fd < 256) user_of_file_descriptor[h->fd] = NULL;
        Free(h);
    }
    return 1;
}

int FsysIsWebDAV(fsys_object_t obj)
{
    return obj && !PrinterClaimsObject(obj) &&
           obj->img->smb_server->webdav != NULL;
}

void FsysClose(fsys_handle_t h)
{
    if(h->is_printer)
    {
        PrinterClose(h);
        return;
    }
    finish_keepalive(h->obj->img);
    VCLog("close handle %d", h->fd);
    SMBClose(h->smb_handle);
    if(h->fd < 256)
        user_of_file_descriptor[h->fd] = NULL;
    Free(h);
}

int FsysGetSize(fsys_handle_t h)
{
    int b;
    unsigned int allocated;

    if(h->is_printer)
        return 0;
    finish_keepalive(h->obj->img);
    /* Round up to a whole block.  The + used to bind tighter than the &,
       and the mask was not inverted, so this returned the length modulo
       the block size rather than the length rounded up to it. */
    /*
        "space currently allocated to file (buffered files only: must be a
        multiple of buffer size)" - so this rounds up to whatever was
        actually reported as the buffer size, not to a fixed figure.
    */
    b = FsysGetBlockSize(h->obj);
    if(b <= 0)
        b = 1;                          /* unbuffered: the figure is ignored */
    /* The rounded allocation can be &80000000 even when the logical
       file length fits in a signed int. Preserve its register bit pattern
       without overflowing signed arithmetic. */
    allocated = ((unsigned int)SMBGetLength(h->obj->smb_obj) +
                 ((unsigned int)b - 1)) & ~((unsigned int)b - 1);
    VCLog("     allocated size asked for: %u (length %d, block %d)",
          allocated, SMBGetLength(h->obj->smb_obj), b);
    return (int)allocated;
}

void FsysEnsureSize(fsys_handle_t h, int s)
{
    int s_now;

    if(h->is_printer)
        return;
    finish_keepalive(h->obj->img);
    s_now = SMBGetLength(h->obj->smb_obj);
    if(s_now < s)
    {
        SMBSetExtent(h->smb_handle, s);
        SMBSetLength(h->obj->smb_obj, s);
    }
}

void FsysLimitSize(fsys_handle_t h, int s)
{
    if(h->is_printer)
    {
        PrinterLimitSize(h, s);
        return;
    }
    finish_keepalive(h->obj->img);
    SMBSetExtent(h->smb_handle, s);
}

fsys_object_t FsysHandle2File(fsys_handle_t h)
{
    if(h->is_printer)
        return PrinterHandle2File(h);
    return h->obj;
}

void FsysRead(void *buf, int pos, int count, fsys_handle_t h)
{
    // static int err_id = 0;

    ExceptTry
    {
        if(h->is_printer)
            Error("Can't read from a printer");
        finish_keepalive(h->obj->img);
        VCLog("read %d bytes at %d", count, pos);
        SMBRead(buf, pos, count, h->smb_handle);
    }
    ExceptCatch
    {
        // show_error(&err_id, "Read", ExceptCaught());
        VCLogErr("read failed", ExceptCaught());
        ExceptRethrow();
    }
}

void FsysWrite(void *buf, int pos, int count, fsys_handle_t h)
{
    if(h->is_printer)
    {
        PrinterWrite(buf, pos, count, h);
        return;
    }
    finish_keepalive(h->obj->img);
    VCLog("write %d bytes at %d", count, pos);
    SMBWrite(buf, pos, count, h->smb_handle);
}

void FsysWriteZeros(void *buf, int pos, int count, fsys_handle_t h)
{
    if(h->is_printer)
    {
        PrinterWrite(buf, pos, count, h);
        return;
    }
    finish_keepalive(h->obj->img);
    VCLog("zero %d bytes at %d", count, pos);
    SMBWrite(buf, pos, count, h->smb_handle);
}
