/*
 * 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.
 *
 * CDDL HEADER END
 *
 * Portions Copyright 2026 RISC OS Developments
 * SPDX-License-Identifier: CDDL-1.0
 */
/*
 * WebDAV filesystem backend. Files are staged on local disc, making random
 * FileSwitch reads/writes independent of server Range/partial-PUT support.
 * Modified files use conditional PUT to detect concurrent remote changes. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include "LanMan98BaseLib/error.h"
#include "LanMan98BaseLib/memory.h"
#include "LanMan98BaseLib/strext.h"
#include "smb.h"
#include "smbsvr.h"
#include "webdav.h"
#include "webhttp.h"
#include "webxml.h"
#include "alphabet.h"

typedef struct dav_file dav_file;
typedef struct {
    web_url url;
    char *user, *pass;
    dav_file *files;
} dav_server;
struct smb_obj_s {
#include "smbobj.h"
    web_entry *listing, *cursor;
    char *pattern;
    int original_time, original_attributes;
};
struct dav_file {
    dav_file *next;
    dav_server *server;
    char path[WEB_PATH_MAX];
    char etag[WEB_ETAG_MAX];
    FILE *file;
    long length;
    int update, dirty;
};

static char *copy(const char *s)
{
    char *p = Malloc(strlen(s)+1); strcpy(p,s); return p;
}
int WebDAVAddress(const char *s)
{
    char prefix[9];
    int i;
    for(i = 0; i < 8 && s[i]; i++) prefix[i] = (char)tolower((unsigned char)s[i]);
    prefix[i] = 0;
    return !strncmp(prefix,"https://",8) || !strncmp(prefix,"http://",7);
}
static void require(web_reply *r, const char *op)
{
    if(r->status == 401) Error("WebDAV authentication failed; check the account and server region");
    if(r->status == 403) Error("WebDAV permission denied");
    if(r->status == 412) Error("WebDAV file changed on the server; local changes were not uploaded");
    if(r->status == 423) Error("WebDAV file is locked by another client");
    if(r->status >= 300 && r->status < 400) Error("WebDAV server redirected the request; use its final collection URL");
    if(r->status < 200 || r->status >= 300 || r->status == 207)
        Error("WebDAV %s failed (HTTP %d)",op,r->status);
}
/* Filing-system mutations must finish before local names/state advance. */
static void completed(web_reply *reply, const char *op)
{
    require(reply,op);
    if(reply->status != 200 && reply->status != 201 && reply->status != 204)
        Error("WebDAV %s completion was not confirmed (HTTP %d)",op,reply->status);
}
static void request(dav_server *s, const char *method, const char *path,
                    const char *headers, FILE *in, long n, FILE *out, web_reply *reply)
{
    WebRequest(&s->url,s->user,s->pass,method,path,headers,in,n,out,reply);
}
static void path_for(smb_obj_t obj, const char *name, char *out)
{
    dav_server *s = obj->svr->webdav;
    char *raw;
    int n;
    const char *path = obj->path ? obj->path : "";
    if(!name) name = "";
    n = strlen(path)+strlen(name);
    if(n >= WEB_PATH_MAX) Error("WebDAV path is too long");
    raw = Malloc(n+1); strcpy(raw,path); strcat(raw,name);
    n = strlen(s->url.base);
    strcpy(out,s->url.base);
    if(n == 1) n = 0;
    if(!WebEscapePath(raw,out+n,WEB_PATH_MAX-n)) { Free(raw); Error("WebDAV path is too long"); }
    Free(raw);
    if(!out[0]) strcpy(out,"/");
}
static char *object_path(smb_obj_t obj, const char *name)
{
    char *path = Malloc(WEB_PATH_MAX);
    ExceptTry { path_for(obj,name,path); }
    ExceptCatch { Free(path); ExceptRethrow(); }
    return path;
}
char *WebDAVMountURL(const char *server, const char *part)
{
    char *out = Malloc(WEB_PATH_MAX), *utf8 = NULL;
    int n = strlen(server);
    while(*part == '/' || *part == '\\') part++;
    ExceptTry
    {
        if(n >= WEB_PATH_MAX-2) Error("WebDAV URL is too long");
        strcpy(out,server);
        if(*part)
        {
            /* The Share field comes from the RISC OS alphabet, just like
               file names. Percent escaping alone would send Latin-1 bytes. */
            utf8 = WebDAVNameToUTF8(part);
            if(n && out[n-1] != '/') out[n++] = '/';
            if(!WebEscapePath(utf8,out+n,WEB_PATH_MAX-n))
                Error("WebDAV collection path is invalid or too long");
        }
    }
    ExceptCatch { if(utf8) Free(utf8); Free(out); ExceptRethrow(); }
    if(utf8) Free(utf8);
    return out;
}
static int unhex(int c)
{
    if(c >= '0' && c <= '9') return c-'0';
    c = tolower((unsigned char)c);
    return c >= 'a' && c <= 'f' ? c-'a'+10 : -1;
}
static char *decode_path(const char *s)
{
    char *out, *p;
    int a, b, c;
    out = p = Malloc(strlen(s)+1);
    while(*s)
    {
        c = (unsigned char)*s++;
        if(c == '%')
        {
            if(!s[0] || !s[1] || (a=unhex(s[0])) < 0 || (b=unhex(s[1])) < 0)
            { Free(out); Error("Invalid WebDAV href escape"); }
            c = a*16+b; s += 2;
            if(c == '/' || c == '\\') { Free(out); Error("Encoded separator in WebDAV href"); }
        }
        if(c < 32 || c == 127 || c == '\\') { Free(out); Error("Invalid WebDAV href character"); }
        *p++ = c;
    }
    while(p > out+1 && p[-1] == '/') p--;
    *p = 0; return out;
}
static const char *href_path(dav_server *s, const char *href)
{
    web_url *parsed;
    const char *p;
    int valid;
    /* A query/fragment is not part of a filename. Encoded %3F/%23 remain
       valid literal characters and are decoded only after this check. */
    if(strchr(href,'?') || strchr(href,'#'))
        Error("WebDAV returned an href with a query or fragment");
    if(*href == '/' && href[1] != '/') return href;
    parsed = Malloc(sizeof(*parsed));
    valid = WebParseURL(href,parsed) && !ci_strcmp(parsed->host,s->url.host) &&
            parsed->port == s->url.port && parsed->secure == s->url.secure;
    Free(parsed);
    if(!valid) Error("WebDAV returned an href outside this server");
    p = strstr(href,"://")+3; p = strchr(p,'/');
    return p ? p : "/";
}
/* Depth-zero responses must describe the requested resource, not merely
   contain plausible properties for some other member of a multistatus. */
static int response_matches(dav_server *s, web_entry *entry, const char *path)
{
    char *wanted = NULL, *actual = NULL;
    int matches;
    ExceptTry
    {
        wanted = decode_path(path);
        actual = decode_path(href_path(s,entry->href));
        matches = !strcmp(wanted,actual);
    }
    ExceptCatch
    {
        if(wanted) Free(wanted); if(actual) Free(actual);
        ExceptRethrow();
    }
    Free(wanted); Free(actual); return matches;
}
static web_entry *listing(dav_server *s, const char *path, int depth)
{
    static const char xml[] = "<?xml version=\"1.0\"?><d:propfind xmlns:d=\"DAV:\" xmlns:r=\"urn:riscostech:lanman98\"><d:prop><d:resourcetype/><d:getcontentlength/><d:getetag/><d:getlastmodified/><r:time/><r:attributes/><d:quota-available-bytes/><d:quota-used-bytes/></d:prop></d:propfind>";
    FILE *in, *out;
    web_reply reply;
    web_entry *entries;
    char headers[80];
    in = NULL; out = NULL; entries = NULL;
    ExceptTry
    {
        in = tmpfile(); out = tmpfile();
        if(!in || !out) Error("Cannot create local WebDAV temporary file");
        if(fwrite(xml,1,sizeof(xml)-1,in) != sizeof(xml)-1) Error("Cannot stage WebDAV request");
        rewind(in);
        sprintf(headers,"Depth: %d\r\nContent-Type: application/xml; charset=utf-8\r\n",depth);
        request(s,"PROPFIND",path,headers,in,sizeof(xml)-1,out,&reply);
        if(reply.status == 207) entries = WebReadListing(out);
        else if(reply.status != 404 || depth != 0)
        {
            require(&reply,"PROPFIND");
            Error("WebDAV PROPFIND did not return a multistatus response (HTTP %d)",reply.status);
        }
    }
    ExceptCatch
    {
        if(in) fclose(in); if(out) fclose(out);
        WebFreeListing(entries); ExceptRethrow();
    }
    fclose(in); fclose(out); return entries;
}
static smb_obj_t construct(smb_server_t svr)
{
    smb_obj_t obj = Malloc(sizeof(*obj)); memset(obj,0,sizeof(*obj)); obj->svr = svr; return obj;
}
static void destruct(smb_obj_t obj)
{
    if(obj->path) Free(obj->path); if(obj->name) Free(obj->name);
    if(obj->ro_name) Free(obj->ro_name); if(obj->pattern) Free(obj->pattern);
    WebFreeListing(obj->listing); Free(obj);
}
static smb_obj_t duplicate(smb_obj_t obj)
{
    smb_obj_t n = construct(obj->svr);
    web_entry *e, **tail, *item;
    ExceptTry
    {
        n->path = obj->path ? copy(obj->path) : NULL;
        n->name = obj->name ? copy(obj->name) : NULL;
        n->pattern = obj->pattern ? copy(obj->pattern) : NULL;
        n->length = obj->length; n->attribute = obj->attribute; n->time = obj->time; n->dirty = obj->dirty;
        n->original_time = obj->original_time; n->original_attributes = obj->original_attributes;
        tail = &n->listing;
        for(e = obj->listing; e; e = e->next)
        {
            item = Malloc(sizeof(*item)); *item = *e; item->href = NULL; item->etag = NULL; item->next = NULL;
            *tail = item; tail = &item->next;
            item->href = copy(e->href); item->etag = e->etag ? copy(e->etag) : NULL;
            if(e == obj->cursor) n->cursor = item;
        }
    }
    ExceptCatch { destruct(n); ExceptRethrow(); }
    return n;
}
static int match(const char *pattern, const char *name)
{
    const char *star = NULL, *retry = NULL;
    if(!strcmp(pattern,"*.*")) pattern = "*";
    while(*name)
    {
        if(*pattern == '?')
        {
            pattern++; name++;
            while(((unsigned char)*name & 0xc0) == 0x80) name++;
        }
        else if(*pattern == *name) { pattern++; name++; }
        else if(*pattern == '*') { star = pattern++; retry = name; }
        else if(star)
        {
            pattern = star+1; retry++;
            while(((unsigned char)*retry & 0xc0) == 0x80) retry++;
            name = retry;
        }
        else return 0;
    }
    while(*pattern == '*') pattern++;
    return !*pattern;
}
static void values(smb_obj_t obj, web_entry *e, const char *name)
{
    char *n = copy(name);
    if(e->length < 0 && !e->directory) { Free(n); Error("WebDAV file has no usable length"); }
    if(obj->name) Free(obj->name); obj->name = n;
    obj->length = e->directory ? 0 : (int)e->length;
    obj->attribute = e->directory ? SMB_SUBDIR : SMB_ARCHIVE;
    if(e->has_attributes) obj->attribute = (e->attributes & ~SMB_SUBDIR) | (e->directory ? SMB_SUBDIR : 0);
    obj->time = e->time; obj->dirty = 0;
    obj->original_time = obj->time; obj->original_attributes = obj->attribute;
}
static int form(smb_obj_t obj, char *name)
{
    char *path, *wanted, *actual;
    web_entry *entries, *e;
    int found = 0;
    path = object_path(obj,name); wanted = NULL; actual = NULL; entries = NULL;
    ExceptTry
    {
        wanted = decode_path(path); entries = listing(obj->svr->webdav,path,0);
        for(e = entries; e; e = e->next)
        {
            actual = decode_path(href_path(obj->svr->webdav,e->href));
            if(!strcmp(actual,wanted) && e->status == 200) { values(obj,e,name); found = 1; }
            Free(actual); actual = NULL;
            if(found) break;
        }
    }
    ExceptCatch
    {
        if(actual) Free(actual); if(wanted) Free(wanted);
        WebFreeListing(entries); Free(path); ExceptRethrow();
    }
    Free(path); Free(wanted); WebFreeListing(entries); return found;
}
static int next(smb_obj_t obj)
{
    char *path, *parent, *full, *leaf;
    web_entry *e;
    int found = 0, n;
    path = object_path(obj,""); parent = NULL; full = NULL;
    ExceptTry
    {
        parent = decode_path(path); n = strlen(parent);
        while((e = obj->cursor) != NULL)
        {
            obj->cursor = e->next;
            full = decode_path(href_path(obj->svr->webdav,e->href));
            leaf = NULL;
            if(n == 1 && parent[0] == '/' && full[0] == '/') leaf = full+1;
            else if(!strncmp(full,parent,n) && full[n] == '/') leaf = full+n+1;
            if(e->status == 200 && leaf && *leaf && !strchr(leaf,'/') &&
               strcmp(leaf,".") && strcmp(leaf,"..") && match(obj->pattern,leaf))
            { values(obj,e,leaf); found = 1; }
            Free(full); full = NULL;
            if(found) break;
        }
    }
    ExceptCatch { if(full) Free(full); if(parent) Free(parent); Free(path); ExceptRethrow(); }
    Free(path); Free(parent); return found;
}
static int first(smb_obj_t obj, char *pattern)
{
    char *path;
    char *p = copy(pattern);
    if(obj->pattern) Free(obj->pattern); obj->pattern = p;
    WebFreeListing(obj->listing); obj->listing = obj->cursor = NULL;
    path = object_path(obj,"");
    ExceptTry { obj->listing = listing(obj->svr->webdav,path,1); }
    ExceptCatch { Free(path); ExceptRethrow(); }
    Free(path); obj->cursor = obj->listing;
    return next(obj);
}
static void flush(smb_obj_t obj)
{
    dav_server *s = obj->svr->webdav;
    FILE *in = NULL, *out = NULL;
    char *path = NULL;
    char xml[512];
    web_reply reply;
    web_entry *entries = NULL, *e;
    int n, accepted = 0;
    if(obj->dirty != 1) return;
    if(obj->time == obj->original_time && obj->attribute == obj->original_attributes)
    { obj->dirty = 0; return; }
    ExceptTry
    {
        path = object_path(obj,obj->name); in = tmpfile(); out = tmpfile();
        if(!in || !out) Error("Cannot stage WebDAV metadata");
        n = sprintf(xml,"<?xml version=\"1.0\"?><d:propertyupdate xmlns:d=\"DAV:\" xmlns:r=\"urn:riscostech:lanman98\"><d:set><d:prop><r:time>%u</r:time><r:attributes>%d</r:attributes></d:prop></d:set></d:propertyupdate>",(unsigned int)obj->time,obj->attribute);
        if(fwrite(xml,1,n,in) != (size_t)n) Error("Cannot stage WebDAV metadata");
        rewind(in);
        request(s,"PROPPATCH",path,"Content-Type: application/xml; charset=utf-8\r\n",in,n,out,&reply);
        if(reply.status == 207)
        {
            entries = listing(s,path,0);
            for(e = entries; e; e = e->next)
                if(e->status == 200 && e->has_time && e->has_attributes &&
                   e->time == obj->time && e->attributes == obj->attribute &&
                   response_matches(s,e,path)) accepted = 1;
            /* Successful PROPPATCH responses commonly echo empty property
               elements. Verify the stored values with PROPFIND instead. */
        }
        else if(reply.status != 403 && reply.status != 405 && reply.status != 501) require(&reply,"PROPPATCH");
    }
    ExceptCatch
    {
        if(path) Free(path); if(in) fclose(in); if(out) fclose(out);
        WebFreeListing(entries); ExceptRethrow();
    }
    Free(path); fclose(in); fclose(out); WebFreeListing(entries);
    if(!accepted && ((obj->attribute ^ obj->original_attributes) & 1))
        Error("WebDAV server does not support persistent RISC OS attributes");
    obj->original_time = obj->time; obj->original_attributes = obj->attribute; obj->dirty = 0;
}
static void space(smb_server_t svr, smb_disc_info_t *info)
{
    dav_server *s = svr->webdav;
    web_entry *entries, *e;
    int found = 0;
    entries = listing(s,s->url.base,0);
    ExceptTry
    {
        for(e = entries; e; e = e->next)
            if(e->status == 200 && e->has_quota_available && e->has_quota_used &&
               e->quota_used <= 0xffffffffu-e->quota_available &&
               response_matches(s,e,s->url.base))
            {
                info->units = 1048576;
                info->free = e->quota_available;
                info->total = e->quota_available+e->quota_used;
                found = 1; break;
            }
    }
    ExceptCatch { WebFreeListing(entries); ExceptRethrow(); }
    WebFreeListing(entries);
    if(!found) Error("This WebDAV server does not supply quota information");
}
void WebDAVCheck(smb_server_t svr)
{
    smb_obj_t root = construct(svr);
    ExceptTry
    {
        root->path = copy("\\");
        if(!form(root,"") || !(root->attribute & SMB_SUBDIR))
            Error("WebDAV mount URL is not an accessible collection");
    }
    ExceptCatch { destruct(root); ExceptRethrow(); }
    destruct(root);
}
void WebDAVConfigure(smb_server_t svr, const char *url, const char *user, const char *pass)
{
    dav_server *s = Malloc(sizeof(*s)); memset(s,0,sizeof(*s)); svr->webdav = s;
    if(!WebParseURL(url,&s->url)) Error("Invalid WebDAV URL; use http(s)://server:port/collection");
    s->user = copy(user); s->pass = copy(pass);
    svr->dialect = NT;
    svr->construct = construct; svr->dup = duplicate; svr->destruct = destruct;
    svr->form = form; svr->first = first; svr->next = next;
    svr->flush = flush; svr->disk_info = space;
    WebDAVCheck(svr);
}
void WebDAVDisconnect(smb_server_t svr)
{
    dav_server *s = svr->webdav;
    if(!s) return;
    if(s->files) Error("Close WebDAV files before disconnecting");
    if(s->user) Free(s->user);
    if(s->pass) { memset(s->pass,0,strlen(s->pass)); Free(s->pass); }
    Free(s); svr->webdav = NULL;
}

smb_obj_t WebDAVMake(smb_obj_t parent, char *name, int directory)
{
    smb_obj_t obj = construct(parent->svr);
    char *path = NULL;
    int n;
    web_reply reply;
    ExceptTry
    {
        n = (parent->path ? strlen(parent->path) : 0)+(parent->name ? strlen(parent->name) : 0)+2;
        obj->path = Malloc(n);
        sprintf(obj->path,"%s%s\\",parent->path ? parent->path : "",parent->name ? parent->name : "");
        obj->name = parent->svr->ro2smb(name);
        path = object_path(obj,obj->name);
        request(parent->svr->webdav,directory ? "MKCOL" : "PUT",path,"If-None-Match: *\r\n",NULL,0,NULL,&reply);
        completed(&reply,directory ? "MKCOL" : "PUT");
        if(!form(obj,obj->name)) Error("New WebDAV object could not be read back");
    }
    ExceptCatch { if(path) Free(path); destruct(obj); ExceptRethrow(); }
    Free(path); return obj;
}
void WebDAVDelete(smb_obj_t obj)
{
    char *path;
    web_reply reply;
    if(obj->dirty == 2) return;
    path = object_path(obj,obj->name);
    ExceptTry
    {
        dav_server *s = obj->svr->webdav;
        dav_file *h;
        int n = strlen(path);
        for(h = s->files; h; h = h->next)
            if(!strncmp(h->path,path,n) && (!h->path[n] || h->path[n] == '/'))
                Error("Close WebDAV files before deleting them");
        request(obj->svr->webdav,"DELETE",path,"",NULL,0,NULL,&reply);
        completed(&reply,"DELETE");
    }
    ExceptCatch { Free(path); ExceptRethrow(); }
    Free(path);
}
static void move_path(smb_obj_t obj, const char *to, int overwrite)
{
    dav_server *s = obj->svr->webdav;
    char *from = NULL, *headers = NULL;
    web_reply reply;
    dav_file *h;
    int n, t;
    ExceptTry
    {
        from = object_path(obj,obj->name); headers = Malloc(WEB_PATH_MAX+400);
        n = strlen(from); t = strlen(to);
        for(h = s->files; h; h = h->next)
        {
            if(!strncmp(h->path,from,n) && (!h->path[n] || h->path[n] == '/'))
            {
                if(t + strlen(h->path+n) >= sizeof(h->path)) Error("Renamed WebDAV path is too long");
            }
            else if(!strncmp(h->path,to,t) && (!h->path[t] || h->path[t] == '/'))
                Error("Close destination WebDAV files before replacing them");
        }
        sprintf(headers,"Destination: %s://%s:%d%s\r\nOverwrite: %c\r\n",s->url.secure ? "https" : "http",s->url.host,s->url.port,to,overwrite ? 'T' : 'F');
        request(s,"MOVE",from,headers,NULL,0,NULL,&reply); completed(&reply,"MOVE");
        for(h = s->files; h; h = h->next)
            if(!strncmp(h->path,from,n) && (!h->path[n] || h->path[n] == '/'))
            {
                memmove(h->path+t,h->path+n,strlen(h->path+n)+1);
                memcpy(h->path,to,t);
            }
    }
    ExceptCatch { if(from) Free(from); if(headers) Free(headers); ExceptRethrow(); }
    Free(from); Free(headers);
}
void WebDAVRename(smb_obj_t obj, char *name)
{
    char *path = NULL, *mapped = obj->svr->ro2smb(name);
    ExceptTry { path = object_path(obj,mapped); move_path(obj,path,0); }
    ExceptCatch { if(path) Free(path); Free(mapped); ExceptRethrow(); }
    Free(path); Free(obj->name); obj->name = mapped;
}
void WebDAVMove(smb_obj_t from, smb_obj_t to)
{
    char *path;
    if(from->svr != to->svr) Error("WebDAV MOVE cannot cross mounts");
    path = object_path(to,to->name);
    ExceptTry { move_path(from,path,1); }
    ExceptCatch { Free(path); ExceptRethrow(); }
    Free(path); from->dirty = 2;
}
/* RFC 9110 entity-tag grammar, without the weak W/ prefix. Accept one
   opaque tag only: a list must not broaden the subsequent If-Match. */
static int strong_etag(const char *tag)
{
    int i, n = strlen(tag);
    unsigned char c;
    if(n < 2 || tag[0] != '"' || tag[n-1] != '"') return 0;
    for(i = 1; i < n-1; i++)
    {
        c = (unsigned char)tag[i];
        if(c < 0x21 || c == '"' || c == 0x7f) return 0;
    }
    return 1;
}
void *WebDAVOpen(smb_obj_t obj, int update)
{
    dav_file *h = Malloc(sizeof(*h)); web_reply reply;
    memset(h,0,sizeof(*h)); h->server = obj->svr->webdav; h->update = update;
    ExceptTry
    {
        path_for(obj,obj->name,h->path);
        h->file = tmpfile(); if(!h->file) Error("Cannot create WebDAV local file cache");
        request(h->server,"GET",h->path,"",NULL,0,h->file,&reply); require(&reply,"GET");
        if(reply.status != 200) Error("WebDAV GET did not return a complete file");
        h->length = reply.length;
        strcpy(h->etag,reply.etag);
        if(update && !strong_etag(h->etag))
            Error("WebDAV server did not supply a strong ETag for safe editing");
    }
    ExceptCatch { if(h->file) fclose(h->file); Free(h); ExceptRethrow(); }
    h->next = h->server->files; h->server->files = h;
    obj->length = (int)h->length;
    return h;
}
void WebDAVRead(void *buf, int pos, int count, void *handle)
{
    dav_file *h = handle; int n;
    if(pos < 0 || count < 0 || pos > 0x7fffffff-count) Error("Invalid WebDAV read range");
    n = pos >= h->length ? 0 : (h->length-pos < count ? (int)(h->length-pos) : count);
    if(n && (fseek(h->file,pos,SEEK_SET) || fread(buf,1,n,h->file) != (size_t)n)) Error("Cannot read WebDAV local cache");
    memset((char *)buf+n,0,count-n);
}
void WebDAVExtent(void *handle, int length)
{
    dav_file *h = handle; unsigned char zero[256]; long n, at;
    if(!h->update || length < 0) Error("Invalid WebDAV file extent");
    if(length > h->length)
    {
        memset(zero,0,sizeof(zero)); at = h->length;
        if(fseek(h->file,at,SEEK_SET)) Error("Cannot extend WebDAV local cache");
        while(at < length)
        {
            n = length-at > (long)sizeof(zero) ? sizeof(zero) : length-at;
            if(fwrite(zero,1,(size_t)n,h->file) != (size_t)n) Error("Cannot extend WebDAV local cache");
            at += n;
        }
    }
    h->length = length; h->dirty = 1;
}
void WebDAVWrite(const void *buf, int pos, int count, void *handle)
{
    dav_file *h = handle;
    if(!h->update || pos < 0 || count < 0 || pos > 0x7fffffff-count) Error("Invalid WebDAV write range");
    if(pos > h->length) WebDAVExtent(h,pos);
    if(fseek(h->file,pos,SEEK_SET) || fwrite(buf,1,count,h->file) != (size_t)count) Error("Cannot write WebDAV local cache");
    if((long)pos+count > h->length) h->length = (long)pos+count;
    h->dirty = 1;
}
/* Cancel only an internally-owned whole-file save after its caller has
   received an error. Never send another request during this cleanup. */
void WebDAVCancel(void *handle)
{
    dav_file *h = handle, **link;
    for(link = &h->server->files; *link && *link != h; link = &(*link)->next) ;
    if(*link) *link = h->next;
    fclose(h->file); Free(h);
}
void WebDAVClose(void *handle)
{
    dav_file *h = handle; web_reply reply; char headers[WEB_ETAG_MAX+32];
    if(h->dirty)
    {
        sprintf(headers,"If-Match: %s\r\n",h->etag); rewind(h->file);
        request(h->server,"PUT",h->path,headers,h->file,h->length,NULL,&reply);
        completed(&reply,"PUT");
        h->dirty = 0;
    }
    WebDAVCancel(h);
}

/* RISC OS names use the existing SMB mapping and reversible UTF-16 escapes.
   This final conversion changes only their wire encoding to UTF-8. */
char *WebDAVNameToUTF8(const char *name)
{
    char *out, *p;
    unsigned int u, low;
    int used;
    if(strlen(name) > WEB_PATH_MAX) Error("WebDAV name is too long");
    out = p = Malloc(strlen(name)*4+1);
    ExceptTry
    {
        while(*name)
        {
            used = AlphabetUnescape(name,&u);
            if(used) name += used;
            else u = AlphabetToUnicode((unsigned char)*name++);
            if(u >= 0xd800 && u <= 0xdbff)
            {
                used = AlphabetUnescape(name,&low);
                if(!used || low < 0xdc00 || low > 0xdfff) Error("Unpaired Unicode surrogate in WebDAV name");
                name += used; u = 0x10000+((u-0xd800)<<10)+(low-0xdc00);
            }
            else if(u >= 0xdc00 && u <= 0xdfff) Error("Unpaired Unicode surrogate in WebDAV name");
            if(u == 0 || u > 0x10ffff) Error("Invalid WebDAV name character");
            if(u < 128) *p++ = (char)u;
            else if(u < 2048) { *p++ = (char)(0xc0|(u>>6)); *p++ = (char)(0x80|(u&63)); }
            else if(u < 65536) { *p++ = (char)(0xe0|(u>>12)); *p++ = (char)(0x80|((u>>6)&63)); *p++ = (char)(0x80|(u&63)); }
            else { *p++ = (char)(0xf0|(u>>18)); *p++ = (char)(0x80|((u>>12)&63)); *p++ = (char)(0x80|((u>>6)&63)); *p++ = (char)(0x80|(u&63)); }
        }
        *p = 0;
    }
    ExceptCatch { Free(out); ExceptRethrow(); }
    return out;
}
char *WebDAVNameFromUTF8(const char *name)
{
    const unsigned char *s = (const unsigned char *)name;
    char *out, *p;
    unsigned int u, min, ro;
    int extra, i, escaped;
    if(strlen(name) > WEB_PATH_MAX) Error("WebDAV name is too long");
    out = p = Malloc(strlen(name)*5+1);
    ExceptTry
    {
        while(*s)
        {
            u = *s++; min = 0; extra = 0;
            if(u >= 0xc2 && u <= 0xdf) { u &= 31; extra = 1; min = 128; }
            else if(u >= 0xe0 && u <= 0xef) { u &= 15; extra = 2; min = 2048; }
            else if(u >= 0xf0 && u <= 0xf4) { u &= 7; extra = 3; min = 65536; }
            else if(u >= 128) Error("Invalid UTF-8 WebDAV filename");
            for(i = 0; i < extra; i++)
            {
                if((*s & 0xc0) != 0x80) Error("Invalid UTF-8 WebDAV filename");
                u = (u<<6)|(*s++ & 63);
            }
            if(u < min || u > 0x10ffff || (u >= 0xd800 && u <= 0xdfff)) Error("Invalid UTF-8 WebDAV filename");
            escaped = 0;
            if(u == '$')
            {
                escaped = 1;
                for(i = 0; i < 4; i++) if(!s[i] || !AlphabetIsHex(s[i])) { escaped = 0; break; }
            }
            ro = AlphabetFromUnicode(u);
            /* These literal wire characters collide with the existing
               RISC OS spelling of different SMB characters. Escape them
               before xsmb2ro so reopening retains the exact wire name.
               Also protect literal wildcard and filesystem-prefix syntax. */
            if(ro == '?' || ro == '+' || ro == '=' || ro == ';' ||
               ro == '<' || ro == '>' || ro == '*' || ro == ':' ||
               ro == 0xa0) escaped = 1;
            if(ro && !escaped) *p++ = (char)ro;
            else if(u <= 65535) p += AlphabetEscape(u,p);
            else
            {
                u -= 0x10000;
                p += AlphabetEscape(0xd800+(u>>10),p);
                p += AlphabetEscape(0xdc00+(u&1023),p);
            }
        }
        *p = 0;
    }
    ExceptCatch { Free(out); ExceptRethrow(); }
    return out;
}
