/*
 * 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 "webio.h"
#include "webxml.h"
#include "alphabet.h"
#ifndef WEB_HOST_TEST
#include "vc.h"
#include "LanMan98BaseLib/thread.h"
#include "LanMan98BaseLib/time.h"
#endif

#define WEB_CERT_PROMPT 2000
#define DAV_STANDARD 0
#define DAV_PCLOUD 1
#define PCLOUD_RETRIES 3

typedef struct dav_file dav_file;
typedef struct dav_created dav_created;
typedef struct {
    web_url url;
    char *user, *pass;
    dav_file *files;
    dav_created *created;
    int metadata;               /* 0 unknown, 1 supported, -1 unavailable */
    int provider;
    int https;
    web_io link;                /* kept HTTP connection, or closed */
} dav_server;
static int https_mounts;
struct smb_obj_s {
#include "smbobj.h"
    web_entry *listing, *cursor;
    char *pattern;
    int original_time, original_attributes;
    int new_file;
};
struct dav_created {
    dav_created *next;
    char path[WEB_PATH_MAX];
    FILE *file;
    long length;
    int local_only;
};
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, wrote_data, created_here;
    int local_stage, renamed;
    smb_obj_t object;
};

static char *copy(const char *s)
{
    char *p = Malloc(strlen(s)+1); strcpy(p,s); return p;
}
static int created_has(dav_server *s, const char *path)
{
    dav_created *item;
    for(item = s->created; item; item = item->next)
        if(!strcmp(item->path,path)) return 1;
    return 0;
}
static dav_created *created_find(dav_server *s, const char *path)
{
    dav_created *item;
    for(item = s->created; item; item = item->next)
        if(!strcmp(item->path,path)) return item;
    return NULL;
}
static dav_created *created_take(dav_server *s, const char *path)
{
    dav_created **link, *item;
    for(link = &s->created; (item = *link) != NULL; link = &item->next)
        if(!strcmp(item->path,path))
        {
            *link = item->next;
            item->next = NULL;
            return item;
        }
    return NULL;
}
static void created_remove(dav_server *s, const char *path, int children)
{
    dav_created **link, *item;
    int n = strlen(path);
    for(link = &s->created; (item = *link) != NULL; )
    {
        if(!strcmp(item->path,path) ||
           (children && !strncmp(item->path,path,n) && item->path[n] == '/'))
        {
            *link = item->next;
            if(item->file) fclose(item->file);
            Free(item);
        }
        else link = &item->next;
    }
}
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;
#ifdef WEB_HTTP_ONLY
    return !strncmp(prefix,"http://",7);
#else
    return !strncmp(prefix,"https://",8) || !strncmp(prefix,"http://",7);
#endif
}
static void require(web_reply *r, const char *op)
{
    if(r->status < 200 || r->status >= 300)
        WebIssue("WebDAV %s failed (HTTP %d)", op, r->status);
    if(r->status == 401) Error("WebDAV authentication failed; check the account and server region");
    if(r->status == 407) Error("WebDAV proxy authentication is required");
    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 %s was redirected; use its final collection URL",op);
    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,&s->link);
#ifndef WEB_HOST_TEST
    VCLog("     WebDAV %s %s -> HTTP %d",method,path,reply->status);
#endif
}
static int pcloud_host(char *host)
{
    return !ci_strcmp(host,"webdav.pcloud.com") ||
           !ci_strcmp(host,"ewebdav.pcloud.com");
}
#ifndef WEB_HOST_TEST
static int delay_complete(void *arg)
{
    return Time() - *(int *)arg >= 0;
}
static void mutation_delay(int attempt)
{
    static const int delay[] = {10,50,200};
    int *until = Malloc(sizeof(*until));
    *until = Time()+delay[attempt];
    ExceptTry { ThreadWait(delay_complete,until); }
    ExceptCatch { Free(until); ExceptRethrow(); }
    Free(until);
}
#else
static void mutation_delay(int attempt) { (void)attempt; }
#endif
/* pCloud can acknowledge a parent collection before a following child
   mutation sees it. Retry only explicit 404/409 responses, and only requests
   whose precondition makes repetition safe. Transport failures and ambiguous
   replies are never replayed. */
static void create_request(dav_server *s, const char *method, const char *path,
                           const char *headers, FILE *in, long n,
                           web_reply *reply)
{
    int attempt;
    for(attempt = 0; ; attempt++)
    {
        if(attempt && in && fseek(in,0,SEEK_SET))
            Error("Cannot retry staged WebDAV data");
        request(s,method,path,headers,in,n,NULL,reply);
        if(s->provider != DAV_PCLOUD ||
           (reply->status != 404 && reply->status != 409) ||
           attempt == PCLOUD_RETRIES)
            return;
#ifndef WEB_HOST_TEST
        VCLog("     pCloud %s retry %d after HTTP %d",method,
              attempt+1,reply->status);
#endif
        mutation_delay(attempt);
    }
}
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;
}
static char *directory_path(smb_obj_t obj, const char *name)
{
    char *path = object_path(obj,name);
    int n = strlen(path);
    if(n > 1 && path[n-1] != '/')
    {
        if(n+1 >= WEB_PATH_MAX) { Free(path); Error("WebDAV path is too long"); }
        path[n++] = '/'; path[n] = 0;
    }
    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_probe(dav_server *s, const char *path, int depth,
                                int *missing, int *redirected)
{
    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(missing) *missing = 0;
        if(redirected) *redirected = 0;
        if(reply.status == 207) entries = WebReadListing(out);
        else if((reply.status == 301 || reply.status == 302 ||
                 reply.status == 307 || reply.status == 308) && redirected)
        {
            *redirected = 1;
        }
        else if(reply.status == 404 && (missing || depth == 0))
        {
            if(missing) *missing = 1;
        }
        else
        {
            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 web_entry *listing(dav_server *s, const char *path, int depth,
                          int *missing)
{
    return listing_probe(s,path,depth,missing,NULL);
}
/* Some providers commit MKCOL and then return a redirect.  Never follow the
   Location header for an authenticated mutation.  A direct Depth: 0 lookup
   of the same canonical path can, however, prove that the collection now
   exists without sending credentials or another mutation elsewhere. */
static int collection_exists(dav_server *s, const char *path)
{
    web_entry *entries, *entry;
    int missing, redirected, found;

    entries = listing_probe(s,path,0,&missing,&redirected);
    found = 0;
    if(!missing && !redirected)
        for(entry = entries; entry; entry = entry->next)
            if(entry->status == 200 && entry->directory &&
               response_matches(s,entry,path))
            {
                found = 1;
                break;
            }
    WebFreeListing(entries);
    return found;
}
/* Most servers canonicalise collection URLs with a trailing slash. UGOS
   advertises collections from the root but returns 404 for that form and
   serves the same href without the slash. Try the advertised equivalent
   only after a 404; other errors retain their normal meaning. */
/* A missing collection is reported through missing where the caller
   distinguishes probing from opening; a NULL missing keeps an error,
   for opening a directory that is not there.  That error is the filing
   system's own missing-directory number, so the layers above read it
   as "not there" - the SMB2 backend reports absence the same way -
   rather than as a failure that aborts whatever was being done. */
static web_entry *collection_listing(dav_server *s, char *path, int depth,
                                     int *missing)
{
    web_entry *entries;
    int absent = 0, n = strlen(path);
    entries = listing(s,path,depth,&absent);
    if(absent && n > 1 && path[n-1] == '/')
    {
        path[n-1] = 0;
        entries = listing(s,path,depth,&absent);
        path[n-1] = '/';
    }
    if(absent && !missing) ErrorNum(0x10003,"Directory not found");
    if(missing) *missing = absent;
    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;
        n->new_file = obj->new_file;
        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_depth(smb_obj_t obj, char *name, char *query,
                      int depth, int directory, int *missing)
{
    char *path, *wanted_path, *wanted, *actual;
    web_entry *entries, *e;
    int found = 0, redirected = 0, collection = directory;
    path = object_path(obj,query); wanted_path = NULL;
    wanted = NULL; actual = NULL; entries = NULL;
    ExceptTry
    {
        wanted_path = object_path(obj,name);
        wanted = decode_path(wanted_path);
        entries = directory ? collection_listing(obj->svr->webdav,path,depth,NULL)
                            : listing_probe(obj->svr->webdav,path,depth,missing,
                                            &redirected);
        if(redirected)
        {
            int n = strlen(path);
            /* pCloud redirects a directory's slashless Depth: 0 lookup to
               its canonical collection URL. Do not follow Location (which
               might change origin and expose credentials); retry only the
               same escaped path with a trailing slash. */
            if(depth != 0 || n == 0 || path[n-1] == '/' ||
               n+1 >= WEB_PATH_MAX)
                Error("WebDAV server redirected the request; use its final collection URL");
            path[n++] = '/'; path[n] = 0;
            entries = collection_listing(obj->svr->webdav,path,depth,NULL);
            collection = 1;
        }
        for(e = entries; e; e = e->next)
        {
            actual = decode_path(href_path(obj->svr->webdav,e->href));
            if(!strcmp(actual,wanted) && e->status == 200 &&
               (!collection || e->directory)) { values(obj,e,name); found = 1; }
            Free(actual); actual = NULL;
            if(found) break;
        }
    }
    ExceptCatch
    {
        if(actual) Free(actual); if(wanted) Free(wanted);
        if(wanted_path) Free(wanted_path);
        WebFreeListing(entries); Free(path); ExceptRethrow();
    }
    Free(path); Free(wanted_path); Free(wanted);
    WebFreeListing(entries); return found;
}
static int form(smb_obj_t obj, char *name)
{
    char *path, *local_name;
    dav_created *created;
    /* pCloud has returned incomplete Depth: 0 resources while supplying the
       same root or child in a Depth: 1 collection response. Exact href
       matching prevents either fallback from accepting a different object. */
    if(!*name && obj->path && !strcmp(obj->path,"\\"))
    {
        if(form_depth(obj,name,name,0,1,NULL)) return 1;
        return form_depth(obj,name,name,1,1,NULL);
    }
    if(form_depth(obj,name,name,0,0,NULL)) return 1;
    /* pCloud can enumerate an object in its parent's Depth: 1 response but
       return no usable Depth: 0 response for that same object. Exact href
       matching keeps this fallback from accepting a sibling. */
    if(*name)
    {
        int missing = 0;

        /* A missing parent means a missing child.  Report that rather
           than failing the lookup: the caller is usually asking whether
           a destination exists yet, before creating it. */
        if(form_depth(obj,name,"",1,0,&missing)) return 1;
        if(missing) return 0;
    }

    /* FileSwitch can close a newly-created, sized placeholder and reopen it
       before assigning the final RISC OS filetype.  Keep that intermediate
       object out of the provider namespace, but make its exact lookup visible
       to FileSwitch until the final typed name is known. */
    path = object_path(obj,name);
    created = created_find(obj->svr->webdav,path);
    Free(path);
    if(!created || !created->local_only) return 0;
    local_name = copy(name);
    if(obj->name) Free(obj->name);
    obj->name = local_name;
    obj->length = (int)created->length;
    obj->attribute = SMB_ARCHIVE;
    obj->time = 0;
    obj->dirty = 0;
    obj->original_time = obj->time;
    obj->original_attributes = obj->attribute;
    obj->new_file = 0;
    return 1;
}
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 = collection_listing(obj->svr->webdav,path,1,NULL); }
    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;
    char *path = NULL;
    char xml[512];
    web_reply reply;
    web_entry *entries = NULL, *e;
    dav_created *staged = NULL;
    int n, accepted = 0, unavailable = 0;
    if(obj->dirty != 1) return;
    if(obj->time == obj->original_time && obj->attribute == obj->original_attributes)
    { obj->dirty = 0; return; }
    if(s->metadata < 0)
    {
        if((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;
        return;
    }
    path = object_path(obj,obj->name);
    staged = created_find(s,path);
    if(staged && staged->local_only)
    {
        /* Never published: an empty file held back so a second lookup
           cannot see a zero-filled placeholder.  There is nothing on
           the server to stamp yet; it uploads at close or disconnect.
           Stamping it here would PROPPATCH a path that does not exist. */
        obj->original_time = obj->time;
        obj->original_attributes = obj->attribute;
        obj->dirty = 0;
        Free(path);
        return;
    }
    ExceptTry
    {
        in = tmpfile();
        if(!in) 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);
        /* Setting these two properties repeats safely, so give pCloud
           a moment when it answers for a file it has only just
           received rather than failing the whole copy. */
        create_request(s,"PROPPATCH",path,"Content-Type: application/xml; charset=utf-8\r\n",in,n,&reply);
        if(reply.status == 207)
        {
            entries = listing(s,path,0,NULL);
            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 == 405 || reply.status == 501 ||
                (reply.status >= 300 && reply.status < 400)) unavailable = 1;
        else if(reply.status != 403) require(&reply,"PROPPATCH");
    }
    ExceptCatch
    {
        if(path) Free(path); if(in) fclose(in);
        WebFreeListing(entries); ExceptRethrow();
    }
    Free(path); fclose(in); WebFreeListing(entries);
    s->metadata = accepted ? 1 : (unavailable ? -1 : 0);
    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,NULL);
    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)
{
    dav_server *s = svr->webdav;
    smb_obj_t root = construct(svr);
    ExceptTry
    {
        root->path = copy("\\");
        if(!form(root,"") || !(root->attribute & SMB_SUBDIR))
        {
            WebIssue("WebDAV root check failed for %s", s ? s->url.base : "(null)");
            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)); WebIOInit(&s->link); svr->webdav = s;
    WebIssue("WebDAV configure URL '%s', user '%s'", url ? url : "(null)", user ? user : "(null)");
#ifndef WEB_HOST_TEST
    VCLogConfigure();
#endif
    if(!WebParseURL(url,&s->url))
    {
        WebIssue("WebDAV URL parse failed: %s", url ? url : "(null)");
        Error(
#ifdef WEB_HTTP_ONLY
        "Invalid WebDAV URL; use http://server:port/collection"
#else
        "Invalid WebDAV URL; use http(s)://server:port/collection"
#endif
        );
    }
    s->provider = pcloud_host(s->url.host) ? DAV_PCLOUD : DAV_STANDARD;
#ifdef WEB_HOST_TEST
    if(getenv("WEB_TEST_PCLOUD")) s->provider = DAV_PCLOUD;
#else
    if(s->provider == DAV_PCLOUD && !s->url.secure)
    {
        WebIssue("pCloud WebDAV requires HTTPS");
        Error("pCloud WebDAV requires its HTTPS regional endpoint");
    }
#endif
    s->url.cert_prompt_cs = WEB_CERT_PROMPT;
    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);
    s->url.cert_prompt_cs = 0;
}
void WebDAVMounted(smb_server_t svr)
{
    dav_server *s = svr->webdav;

    /* Count a share only once it is actually mounted. A login that fails
       on a slow link is torn down by the same disconnect, and that must
       not be the last share that unloads TLSManager. */
    if(!s || !s->url.secure || s->https)
        return;
    s->https = 1;
    https_mounts++;
    WebIssue("WebDAV mounted (HTTPS active mounts: %d)", https_mounts);
}
void WebDAVDisconnect(smb_server_t svr)
{
    dav_server *s = svr->webdav;
    dav_created *item;
    web_reply reply;
    if(!s) return;
    WebIssue("WebDAV disconnect for %s (https=%d, active mounts=%d)",
             s->url.base, s->https, https_mounts);
    if(s->files) Error("Close WebDAV files before disconnecting");
    for(item = s->created; item; item = item->next)
        if(item->local_only)
        {
            if(fflush(item->file) || fseek(item->file,0,SEEK_SET))
                Error("Cannot prepare pending WebDAV file for upload");
            create_request(s,"PUT",item->path,"If-None-Match: *\r\n",
                           item->file,item->length,&reply);
            completed(&reply,"PUT");
            item->local_only = 0;
        }
    WebIOClose(&s->link);
    if(s->https && --https_mounts <= 0)
    {
        https_mounts = 0;
#ifndef WEB_HTTP_ONLY
        WebIssue("WebDAV last HTTPS share disconnected; killing TLSManager");
        _kernel_oscli("RMKill TLSManager");
#endif
    }
    if(s->user) Free(s->user);
    if(s->pass) { memset(s->pass,0,strlen(s->pass)); Free(s->pass); }
    while((item = s->created) != NULL)
    {
        s->created = item->next;
        if(item->file) fclose(item->file);
        Free(item);
    }
    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 = directory ? directory_path(obj,obj->name)
                         : object_path(obj,obj->name);
        if(directory)
        {
            int attempt, confirmed;

            create_request(parent->svr->webdav,"MKCOL",path,
                           "If-None-Match: *\r\n",NULL,0,&reply);
            if(reply.status == 301 || reply.status == 302 ||
               reply.status == 307 || reply.status == 308)
            {
                confirmed = 0;
                for(attempt = 0; attempt <= PCLOUD_RETRIES; attempt++)
                {
                    if(attempt) mutation_delay(attempt-1);
                    if(collection_exists(parent->svr->webdav,path))
                    {
                        confirmed = 1;
                        break;
                    }
                }
                if(!confirmed)
                    Error("Redirected WebDAV MKCOL was not confirmed at its canonical URL");
            }
            else completed(&reply,"MKCOL");
            obj->length = 0;
            obj->attribute = SMB_SUBDIR;
            obj->time = 0;
            obj->original_time = obj->time;
            obj->original_attributes = obj->attribute;
            obj->dirty = 0;
        }
        else
        {
            /* Do not publish a zero-filled placeholder. FileSwitch creates
               the catalogue entry before it supplies the bytes, and a
               second lookup can select that empty object while a RISC OS
               filetype rename is still pending. Keep a new file local until
               close, then create it with one conditional PUT. */
            obj->length = 0;
            obj->attribute = SMB_ARCHIVE;
            obj->time = 0;
            obj->original_time = obj->time;
            obj->original_attributes = obj->attribute;
            obj->dirty = 0;
            obj->new_file = 1;
        }
    }
    ExceptCatch { if(path) Free(path); destruct(obj); ExceptRethrow(); }
    Free(path); return obj;
}
void WebDAVDelete(smb_obj_t obj)
{
    char *path;
    web_reply reply;
    int local_delete = 0;
    if(obj->dirty == 2) return;
    path = (obj->attribute & SMB_SUBDIR)
             ? directory_path(obj,obj->name) : 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");
        {
            dav_created *created = created_find(s,path);
            if(created && created->local_only)
            {
                created_remove(s,path,(obj->attribute & SMB_SUBDIR) != 0);
                local_delete = 1;
            }
        }
        if(!local_delete)
            request(obj->svr->webdav,"DELETE",path,"",NULL,0,NULL,&reply);
        /* pCloud accepts its canonical collection form for MKCOL and
           PROPFIND, but can reject DELETE on that same trailing-slash URL
           with 403.  A rejected DELETE has not committed, so retry the
           equivalent slashless path only for the identified provider. */
        if(!local_delete && (reply.status == 404 ||
            (reply.status == 403 && s->provider == DAV_PCLOUD)) &&
           n > 1 && path[n-1] == '/')
        {
            path[n-1] = 0;
            request(obj->svr->webdav,"DELETE",path,"",NULL,0,NULL,&reply);
        }
        if(!local_delete)
        {
            completed(&reply,"DELETE");
            created_remove(s,path,(obj->attribute & SMB_SUBDIR) != 0);
        }
    }
    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");
        }
        {
            dav_created *item;
            for(item = s->created; item; item = item->next)
                if(!strncmp(item->path,from,n) &&
                   (!item->path[n] || item->path[n] == '/') &&
                   t + strlen(item->path+n) >= sizeof(item->path))
                    Error("Renamed WebDAV path is too long");
        }
        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);
            }
        {
            dav_created *item;
            for(item = s->created; item; item = item->next)
                if(!strncmp(item->path,from,n) &&
                   (!item->path[n] || item->path[n] == '/'))
                {
                    memmove(item->path+t,item->path+n,
                            strlen(item->path+n)+1);
                    memcpy(item->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)
{
    dav_file *h;
    dav_created *created;
    dav_server *s = obj->svr->webdav;
    char *path = NULL, *old_path = NULL;
    web_reply reply;
    int local_open = 0;
    char *mapped = obj->svr->ro2smb(name);
    ExceptTry
    {
        path = object_path(obj,mapped);
        if(obj->new_file)
        {
            /* A new FileSwitch file can acquire its RISC OS filetype while
               its local cache is already open.  There is no remote object to
               MOVE yet.  Retarget every cache opened on the provisional name
               so the single closing PUT creates only the final typed name. */
            old_path = object_path(obj,obj->name);
            for(h = s->files; h; h = h->next)
                if(!strcmp(h->path,old_path))
                {
                    strcpy(h->path,path);
                    h->renamed = 1;
                }
            created = created_find(s,old_path);
            if(created && created->local_only) strcpy(created->path,path);
        }
        else
        {
            old_path = object_path(obj,obj->name);
            created = created_find(s,old_path);
            for(h = s->files; h; h = h->next)
                if(h->local_stage && !strcmp(h->path,old_path))
                {
                    strcpy(h->path,path);
                    h->renamed = 1;
                    local_open = 1;
                }
            if(local_open)
            {
                /* The local cache will create the final typed object when it
                   closes. There is no provisional remote object to move. */
            }
            else if(created && created->local_only)
            {
                if(fflush(created->file) ||
                   fseek(created->file,0,SEEK_SET))
                    Error("Cannot prepare WebDAV local file cache for upload");
                create_request(s,"PUT",path,"If-None-Match: *\r\n",
                               created->file,created->length,&reply);
                completed(&reply,"PUT");
                fclose(created->file);
                created->file = NULL;
                created->local_only = 0;
                strcpy(created->path,path);
            }
            else
            {
                move_path(obj,path,0);
                if(created) created_remove(s,path,0);
            }
        }
    }
    ExceptCatch
    {
        if(old_path) Free(old_path);
        if(path) Free(path);
        Free(mapped);
        ExceptRethrow();
    }
    if(old_path) Free(old_path);
    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;
    dav_created *stage = NULL;
    memset(h,0,sizeof(*h)); h->server = obj->svr->webdav; h->update = update;
    h->object = obj;
    ExceptTry
    {
        path_for(obj,obj->name,h->path);
        stage = created_find(h->server,h->path);
        if(stage && stage->local_only)
        {
            stage = created_take(h->server,h->path);
            h->file = stage->file;
            h->length = stage->length;
            h->created_here = 1;
            h->local_stage = 1;
        }
        else
        {
            stage = NULL;
            h->file = tmpfile();
            if(!h->file) Error("Cannot create WebDAV local file cache");
            /* Use a generous 64KB stream buffer for disk caching */
            setvbuf(h->file, NULL, _IOFBF, 65536);
        }
        if(obj->new_file && !stage)
        {
            if(!update) Error("New WebDAV file is not ready for reading");
            h->length = 0;
        }
        else if(!stage)
        {
            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);
            h->created_here = created_has(h->server,h->path);
        }
        /* Publish the update stream in its reading state before handing the
           cache to FileSwitch. Each later read seeks too, but the native
           stdio implementation must not retain the HTTP output direction. */
        if(fflush(h->file) || fseek(h->file,0,SEEK_SET))
            Error("Cannot prepare WebDAV local file cache");
#ifndef WEB_HOST_TEST
        VCLog("     WebDAV GET staged %ld bytes",h->length);
#endif
        if(update && !obj->new_file && !strong_etag(h->etag) &&
           !h->created_here)
            Error("WebDAV server did not supply a strong ETag for safe editing");
    }
    ExceptCatch
    {
        if(stage)
        {
            stage->file = h->file;
            stage->length = h->length;
            stage->next = h->server->created;
            h->server->created = stage;
            h->file = NULL;
        }
        if(h->file) fclose(h->file);
        Free(h);
        ExceptRethrow();
    }
    if(stage) Free(stage);
    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;
    if(count) h->wrote_data = 1;
    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;
    if(h->file) fclose(h->file);
    Free(h);
}
void WebDAVClose(void *handle)
{
    dav_file *h = handle; web_reply reply; char headers[WEB_ETAG_MAX+32];
    dav_created *mark = NULL;
    int was_new;
    if(!h->renamed &&
       ((h->object->new_file && !h->wrote_data) ||
        (h->local_stage && !h->wrote_data)))
    {
        /* Nothing written through this handle: hold a new file back so
           a second lookup cannot see a zero-filled placeholder, or keep
           an already staged one where it is.  A handle that did take
           bytes must fall through and publish them - reopening a staged
           file to stream its contents is the normal Filer copy sequence,
           and holding those bytes back again would report success while
           the server never receives them. */
        mark = Malloc(sizeof(*mark));
        memset(mark,0,sizeof(*mark));
        strcpy(mark->path,h->path);
        mark->file = h->file;
        mark->length = h->length;
        mark->local_only = 1;
        created_remove(h->server,h->path,0);
        mark->next = h->server->created;
        h->server->created = mark;
        h->file = NULL;
        h->object->new_file = 0;
        h->object->length = (int)h->length;
        h->object->dirty = 0;
        WebDAVCancel(h);
        return;
    }
    if(h->dirty || h->object->new_file ||
       (h->local_stage && h->renamed))
    {
        was_new = h->object->new_file || h->local_stage;
        if(was_new)
            strcpy(headers,"If-None-Match: *\r\n");
        else if(h->created_here && !strong_etag(h->etag))
            /* FileSwitch can create a sized zero file, close it, then reopen
               it immediately to stream the real contents.  pCloud omits a
               usable ETag on that readback.  The object was conditionally
               created by this mount, so require that it still exists rather
               than falling back to an unconditional replacement. */
            strcpy(headers,"If-Match: *\r\n");
        else
            sprintf(headers,"If-Match: %s\r\n",h->etag);
        ExceptTry
        {
            /* Commit all positioned cache writes before the closing PUT. */
            if(fflush(h->file) || fseek(h->file,0,SEEK_SET))
                Error("Cannot prepare WebDAV local cache for upload");
            if(was_new)
                create_request(h->server,"PUT",h->path,headers,h->file,
                               h->length,&reply);
            else
                request(h->server,"PUT",h->path,headers,h->file,h->length,
                        NULL,&reply);
            completed(&reply,"PUT");
        }
        ExceptCatch
        {
            ExceptRethrow();
        }
        if(h->wrote_data) created_remove(h->server,h->path,0);
        h->object->new_file = 0;
        h->object->length = (int)h->length;
        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;
}
