/*
 * 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
 */
/*
 * Bounded namespace-aware XML reader for DAV multistatus responses.
 * DTDs and custom entities are rejected. Properties are accepted only from
 * successful propstat blocks; failed properties never overwrite good ones. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include "webxml.h"
#include "webhttp.h"
#ifdef WEB_HOST_TEST
#include "webtest.h"
#else
#include "LanMan98BaseLib/error.h"
#include "LanMan98BaseLib/memory.h"
#endif
#define XML_LIMIT WEB_XML_MAX
#define NODE_LIMIT 262144
#define DEPTH_LIMIT 64
#define NS_LIMIT 256

typedef struct {
    const char *qname, *text, *end;
    int qlen, parent, after, dav;
    char name[64];
} node;
typedef struct { const char *prefix, *uri; int plen, ulen, depth; } binding;
typedef struct {
    char *data;
    node *nodes;
    int count, capacity;
    web_entry *entries;
    binding ns[NS_LIMIT];
} document;

static int namechar(int c)
{
    return isalnum((unsigned char)c) || c == ':' || c == '_' || c == '-' || c == '.';
}

static void utf8(char **out, unsigned long c)
{
    if(c == 0 || c > 0x10ffff || (c >= 0xd800 && c <= 0xdfff) ||
       (c < 32 && c != 9 && c != 10 && c != 13)) Error("Invalid XML character");
    if(c < 128) *(*out)++ = (char)(c);
    else if(c < 2048) { *(*out)++ = (char)(0xc0 | (c>>6)); *(*out)++ = (char)(0x80 | (c&63)); }
    else if(c < 65536) { *(*out)++ = (char)(0xe0 | (c>>12)); *(*out)++ = (char)(0x80 | ((c>>6)&63)); *(*out)++ = (char)(0x80 | (c&63)); }
    else { *(*out)++ = (char)(0xf0 | (c>>18)); *(*out)++ = (char)(0x80 | ((c>>12)&63)); *(*out)++ = (char)(0x80 | ((c>>6)&63)); *(*out)++ = (char)(0x80 | (c&63)); }
}

static char *text(const node *n)
{
    const char *p, *end, *q;
    char *result, *out;
    unsigned long c;
    int base, d;
    p = n->text; end = n->end;
    result = Malloc(end-p+1); out = result;
    ExceptTry
    {
        while(p < end)
        {
            if(*p == '<')
            {
                if(end-p >= 9 && !memcmp(p,"<![CDATA[",9))
                {
                    q = strstr(p+9,"]]>");
                    if(!q || q > end) Error("Unterminated XML CDATA");
                    memcpy(out,p+9,q-p-9); out += q-p-9; p = q+3; continue;
                }
                if(end-p >= 4 && !memcmp(p,"<!--",4))
                {
                    q = strstr(p+4,"-->");
                    if(!q || q > end) Error("Unterminated XML comment");
                    p = q+3; continue;
                }
                Error("Unexpected nested XML value");
            }
            if(*p != '&') { *out++ = *p++; continue; }
            q = ++p;
            while(q < end && *q != ';' && q-p < 12) q++;
            if(q == end || *q != ';') Error("Invalid XML entity");
            if(q-p == 3 && !memcmp(p,"amp",3)) *out++ = '&';
            else if(q-p == 2 && !memcmp(p,"lt",2)) *out++ = '<';
            else if(q-p == 2 && !memcmp(p,"gt",2)) *out++ = '>';
            else if(q-p == 4 && !memcmp(p,"quot",4)) *out++ = '"';
            else if(q-p == 4 && !memcmp(p,"apos",4)) *out++ = '\'';
            else if(*p == '#')
            {
                p++; base = 10; c = 0;
                if(p < q && *p == 'x') { base = 16; p++; }
                if(p == q) Error("Empty XML character reference");
                while(p < q)
                {
                    d = isdigit((unsigned char)*p) ? *p-'0' :
                        (base == 16 && isxdigit((unsigned char)*p) ? tolower((unsigned char)*p)-'a'+10 : -1);
                    if(d < 0 || d >= base || c > (0x10ffffUL-d)/base) Error("Invalid XML character reference");
                    c = c*base+d; p++;
                }
                utf8(&out,c);
            }
            else Error("External or unknown XML entity");
            p = q+1;
        }
        *out = 0;
    }
    ExceptCatch { Free(result); ExceptRethrow(); }
    return result;
}

static void parse(document *doc)
{
    const char *p, *q, *name, *aname, *value, *local;
    int stack[DEPTH_LIMIT], depth = 0, nsn = 0, n, i, alen, vlen, empty, quote, plen;
    binding *ns = doc->ns;
    node *t;
    p = doc->data;
    if(!memcmp(p,"\xef\xbb\xbf",3)) p += 3;
    while(*p)
    {
        if(*p != '<')
        {
            if(!depth && !isspace((unsigned char)*p)) Error("Text outside XML root");
            if((unsigned char)*p < 32 && *p != '\t' && *p != '\n' && *p != '\r') Error("Invalid XML character");
            p++; continue;
        }
        if(!strncmp(p,"<!--",4))
        {
            q = strstr(p+4,"-->"); if(!q) Error("Unterminated XML comment"); p = q+3; continue;
        }
        if(!strncmp(p,"<?",2))
        {
            q = strstr(p+2,"?>"); if(!q) Error("Unterminated XML declaration"); p = q+2; continue;
        }
        if(!strncmp(p,"<![CDATA[",9))
        {
            if(!depth) Error("CDATA outside XML root");
            q = strstr(p+9,"]]>"); if(!q) Error("Unterminated XML CDATA"); p = q+3; continue;
        }
        if(p[1] == '!') Error("XML DTDs and declarations are not accepted");
        if(p[1] == '/')
        {
            q = p; p += 2; name = p;
            while(namechar(*p)) p++;
            if(!depth) Error("Unmatched XML closing tag");
            t = &doc->nodes[stack[depth-1]];
            if(p-name != t->qlen || memcmp(name,t->qname,t->qlen)) Error("Mismatched XML closing tag");
            while(isspace((unsigned char)*p)) p++;
            if(*p++ != '>') Error("Malformed XML closing tag");
            t->end = q; t->after = doc->count;
            while(nsn && ns[nsn-1].depth == depth) nsn--;
            depth--; continue;
        }
        name = ++p;
        while(namechar(*p)) p++;
        n = p-name;
        if(!n || depth >= DEPTH_LIMIT || (!depth && doc->count)) Error("Invalid XML element nesting");
        if(doc->count == doc->capacity)
        {
            int cap = doc->capacity ? doc->capacity*2 : 128;
            node *grown;
            if(cap > NODE_LIMIT) Error("Too many WebDAV XML elements");
            grown = Malloc(cap*sizeof(node));
            if(doc->count) memcpy(grown,doc->nodes,doc->count*sizeof(node));
            if(doc->nodes) Free(doc->nodes);
            doc->nodes = grown; doc->capacity = cap;
        }
        i = doc->count++; t = &doc->nodes[i]; memset(t,0,sizeof(*t));
        t->qname = name; t->qlen = n; t->parent = depth ? stack[depth-1] : -1;
        stack[depth++] = i;
        local = name; plen = 0;
        for(q = name; q < name+n; q++) if(*q == ':') { plen = q-name; local = q+1; }
        if(name+n-local >= (int)sizeof(t->name)) Error("Oversized XML element name");
        memcpy(t->name,local,name+n-local);
        empty = 0;
        for(;;)
        {
            while(isspace((unsigned char)*p)) p++;
            if(*p == '/') { empty = 1; p++; }
            if(*p == '>') { p++; break; }
            if(empty || !*p) Error("Malformed XML opening tag");
            aname = p; while(namechar(*p)) p++; alen = p-aname;
            if(!alen) Error("Malformed XML attribute");
            while(isspace((unsigned char)*p)) p++;
            if(*p++ != '=') Error("Missing XML attribute value");
            while(isspace((unsigned char)*p)) p++;
            quote = *p++;
            if(quote != '\'' && quote != '"') Error("Unquoted XML attribute");
            value = p; while(*p && *p != quote && *p != '<') p++;
            if(*p != quote) Error("Unterminated XML attribute");
            vlen = p-value; p++;
            if((alen == 5 && !memcmp(aname,"xmlns",5)) || (alen > 6 && !memcmp(aname,"xmlns:",6)))
            {
                if(nsn == NS_LIMIT) Error("Too many XML namespaces");
                ns[nsn].prefix = aname+6; ns[nsn].plen = alen == 5 ? 0 : alen-6;
                ns[nsn].uri = value; ns[nsn].ulen = vlen; ns[nsn++].depth = depth;
            }
        }
        t->dav = 0;
        for(n = nsn-1; n >= 0; n--)
            if(ns[n].plen == plen && (!plen || !memcmp(ns[n].prefix,name,plen)))
            { t->dav = ns[n].ulen == 4 && !memcmp(ns[n].uri,"DAV:",4) ? 1 :
                          (ns[n].ulen == 23 && !memcmp(ns[n].uri,"urn:riscostech:lanman98",23) ? 2 : 0); break; }
        if(plen && n < 0) Error("Unbound XML namespace prefix");
        t->text = p;
        if(empty)
        {
            t->end = p; t->after = doc->count;
            while(nsn && ns[nsn-1].depth == depth) nsn--;
            depth--;
        }
    }
    if(depth || !doc->count) Error("Incomplete WebDAV XML");
    if(doc->nodes[0].dav != 1 || strcmp(doc->nodes[0].name,"multistatus")) Error("Expected DAV multistatus XML");
}

static int child(document *d, int parent, const char *name)
{
    int i;
    for(i = parent+1; i < d->nodes[parent].after; i = d->nodes[i].after)
        if(d->nodes[i].dav == 1 && !strcmp(d->nodes[i].name,name)) return i;
    return -1;
}
static int status(document *d, int parent)
{
    int i, code = 0;
    char *s;
    i = child(d,parent,"status"); if(i < 0) return 0;
    s = text(&d->nodes[i]);
    /* Bound every digit field: sscanf integer overflow is undefined, and
       a longer status such as 2000 must never be mistaken for success. */
    if(strlen(s) >= 12 && (!strncmp(s,"HTTP/1.1 ",9) || !strncmp(s,"HTTP/1.0 ",9)) &&
       isdigit((unsigned char)s[9]) && isdigit((unsigned char)s[10]) &&
       isdigit((unsigned char)s[11]) && (!s[12] || s[12] == ' '))
        code = (s[9]-'0')*100+(s[10]-'0')*10+s[11]-'0';
    Free(s); return code;
}

static int custom(document *d, int parent, const char *name)
{
    int i;
    for(i = parent+1; i < d->nodes[parent].after; i = d->nodes[i].after)
        if(d->nodes[i].dav == 2 && !strcmp(d->nodes[i].name,name)) return i;
    return -1;
}
/* IMF-fixdate is UTC; do not use the host's local-time mktime conversion. */
static int decimal(const char *s, unsigned long limit, unsigned long *result)
{
    unsigned long n = 0;
    if(!*s) return 0;
    while(*s)
    {
        unsigned int digit = (unsigned char)*s++ - '0';
        if(digit > 9 || n > (limit-digit)/10) return 0;
        n = n*10+digit;
    }
    *result = n; return 1;
}
/* Decimal bytes converted to MiB without relying on a 64-bit C long.
   Available space rounds down; used space rounds up. */
static int quota(const char *s, int round_up, unsigned int *result)
{
    unsigned int q = 0, r = 0, digit, carry;
    if(!*s) return 0;
    while(*s)
    {
        digit = (unsigned char)*s++ - '0'; if(digit > 9) return 0;
        r = r*10+digit; carry = r/1048576; r %= 1048576;
        if(q > (0xffffffffu-carry)/10) return 0;
        q = q*10+carry;
    }
    if(round_up && r) { if(q == 0xffffffffu) return 0; q++; }
    *result = q; return 1;
}
static int http_date(const char *s)
{
    static const char months[] = "JanFebMarAprMayJunJulAugSepOctNovDec";
    static const int lengths[] = {31,28,31,30,31,30,31,31,30,31,30,31};
    char weekday[4] = {0}, month[4] = {0}, zone[4] = {0};
    int day, year, hour, minute, second, m, y, leap;
    unsigned long days = 0, seconds;
    if(sscanf(s,"%3s %2d %3s %4d %2d:%2d:%2d %3s",weekday,&day,month,&year,&hour,&minute,&second,zone) != 8)
    {
        if(sscanf(s,"%3[^,], %2d %3s %4d %2d:%2d:%2d %3s",weekday,&day,month,&year,&hour,&minute,&second,zone) != 8) return 0;
    }
    for(m = 0; m < 12; m++) if(!memcmp(month,months+m*3,3)) break;
    if(m == 12 || year < 1970 || year > 2105 || hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59 || strcmp(zone,"GMT")) return 0;
    leap = year%4 == 0 && (year%100 != 0 || year%400 == 0);
    if(day < 1 || day > lengths[m]+(m == 1 && leap)) return 0;
    for(y = 1970; y < year; y++) days += 365UL+(unsigned long)(y%4 == 0 && (y%100 != 0 || y%400 == 0));
    for(y = 0; y < m; y++) days += (unsigned long)lengths[y]+(unsigned long)(y == 1 && leap);
    days += (unsigned long)(day-1); seconds = days*86400UL+hour*3600UL+minute*60UL+second;
    return (int)(unsigned int)seconds;
}
void WebFreeListing(web_entry *e)
{
    web_entry *next;
    while(e)
    {
        next = e->next;
        if(e->href) Free(e->href);
        if(e->etag) Free(e->etag);
        Free(e); e = next;
    }
}

web_entry *WebReadListing(FILE *f)
{
    document *d;
    web_entry **tail, *e, *result;
    long size;
    int i, j, k, prop, v;
    char *s;
    d = Malloc(sizeof(*d)); memset(d,0,sizeof(*d));
    ExceptTry
    {
        if(fseek(f,0,SEEK_END) || (size = ftell(f)) < 0 || size > XML_LIMIT || size < 3)
            Error("WebDAV XML exceeds the size limit");
        rewind(f); d->data = Malloc((int)size+1);
        if(fread(d->data,1,(size_t)size,f) != (size_t)size) Error("Cannot read WebDAV XML");
        d->data[size] = 0;
        if(memchr(d->data,0,(size_t)size)) Error("Embedded zero in WebDAV XML");
        parse(d); tail = &d->entries;
        for(i = 1; i < d->count; i = d->nodes[i].after)
        {
            if(d->nodes[i].dav != 1 || strcmp(d->nodes[i].name,"response")) continue;
            e = Malloc(sizeof(*e)); memset(e,0,sizeof(*e)); *tail = e; tail = &e->next;
            e->length = -1; e->status = status(d,i);
            k = child(d,i,"href"); if(k < 0) Error("DAV response omitted its href");
            e->href = text(&d->nodes[k]);
            for(j = i+1; j < d->nodes[i].after; j = d->nodes[j].after)
            {
                if(d->nodes[j].dav != 1 || strcmp(d->nodes[j].name,"propstat") || status(d,j) != 200) continue;
                prop = child(d,j,"prop"); if(prop < 0) continue;
                e->status = 200;
                v = child(d,prop,"resourcetype");
                if(v >= 0) e->directory = child(d,v,"collection") >= 0;
                v = child(d,prop,"getcontentlength");
                if(v >= 0)
                {
                    s = text(&d->nodes[v]);
                    {
                        unsigned long value = 0;
                        k = !decimal(s,0x7fffffffUL,&value);
                        e->length = (long)value;
                    }
                    Free(s); if(k) Error("Invalid or oversized WebDAV file length");
                }
                v = child(d,prop,"getlastmodified");
                if(v >= 0 && !e->has_time) { s = text(&d->nodes[v]); e->time = http_date(s); Free(s); }
                v = custom(d,prop,"time");
                if(v >= 0)
                {
                    unsigned long value;
                    s = text(&d->nodes[v]);
                    k = !decimal(s,0xffffffffUL,&value);
                    Free(s); if(k) Error("Invalid WebDAV timestamp property");
                    e->time = (int)(unsigned int)value; e->has_time = 1;
                }
                v = custom(d,prop,"attributes");
                if(v >= 0)
                {
                    unsigned long value;
                    s = text(&d->nodes[v]); k = !decimal(s,255,&value);
                    Free(s); if(k) Error("Invalid WebDAV attributes property");
                    e->attributes = (int)value; e->has_attributes = 1;
                }
                v = child(d,prop,"quota-available-bytes");
                if(v >= 0) { s = text(&d->nodes[v]); e->has_quota_available = quota(s,0,&e->quota_available); Free(s); }
                v = child(d,prop,"quota-used-bytes");
                if(v >= 0) { s = text(&d->nodes[v]); e->has_quota_used = quota(s,1,&e->quota_used); Free(s); }
                v = child(d,prop,"getetag");
                if(v >= 0)
                {
                    /* Keep the old tag owned until replacement succeeds. */
                    s = text(&d->nodes[v]);
                    if(e->etag) Free(e->etag);
                    e->etag = s;
                }
            }
        }
    }
    ExceptCatch
    {
        WebFreeListing(d->entries);
        if(d->nodes) Free(d->nodes);
        if(d->data) Free(d->data);
        Free(d); ExceptRethrow();
    }
    result = d->entries; Free(d->nodes); Free(d->data); Free(d);
    return result;
}
