ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
webdav.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file webdav.cpp
6 * @brief WebDAV server core (RFC 4918): method classification, header parsing,
7 * and the 207 Multi-Status XML builder. Pure - no sockets, no filesystem.
8 */
9
12
13#if PC_ENABLE_WEBDAV
14
15#include <string.h>
16
17WebDavMethod pc_webdav_method(const char *m)
18{
19 if (!m)
20 {
21 return WebDavMethod::DAV_M_UNSUPPORTED;
22 }
23 if (!strcmp(m, "OPTIONS"))
24 {
25 return WebDavMethod::DAV_M_OPTIONS;
26 }
27 if (!strcmp(m, "GET"))
28 {
29 return WebDavMethod::DAV_M_GET;
30 }
31 if (!strcmp(m, "HEAD"))
32 {
33 return WebDavMethod::DAV_M_HEAD;
34 }
35 if (!strcmp(m, "PUT"))
36 {
37 return WebDavMethod::DAV_M_PUT;
38 }
39 if (!strcmp(m, "DELETE"))
40 {
41 return WebDavMethod::DAV_M_DELETE;
42 }
43 if (!strcmp(m, "PROPFIND"))
44 {
45 return WebDavMethod::DAV_M_PROPFIND;
46 }
47 if (!strcmp(m, "PROPPATCH"))
48 {
49 return WebDavMethod::DAV_M_PROPPATCH;
50 }
51 if (!strcmp(m, "MKCOL"))
52 {
53 return WebDavMethod::DAV_M_MKCOL;
54 }
55 if (!strcmp(m, "COPY"))
56 {
57 return WebDavMethod::DAV_M_COPY;
58 }
59 if (!strcmp(m, "MOVE"))
60 {
61 return WebDavMethod::DAV_M_MOVE;
62 }
63 if (!strcmp(m, "LOCK"))
64 {
65 return WebDavMethod::DAV_M_LOCK;
66 }
67 if (!strcmp(m, "UNLOCK"))
68 {
69 return WebDavMethod::DAV_M_UNLOCK;
70 }
71 return WebDavMethod::DAV_M_UNSUPPORTED;
72}
73
74int pc_webdav_depth(const char *depth_hdr, int dflt)
75{
76 if (!depth_hdr || !depth_hdr[0])
77 {
78 return dflt;
79 }
80 if (!strcmp(depth_hdr, "0"))
81 {
82 return 0;
83 }
84 if (!strcmp(depth_hdr, "1"))
85 {
86 return 1;
87 }
88 if (!strcmp(depth_hdr, "infinity"))
89 {
90 return PC_DAV_DEPTH_INFINITY;
91 }
92 return dflt;
93}
94
95// Append a NUL-terminated string if it fits; returns false (leaving *len and the
96// NUL terminator intact) when it would overflow.
97static bool app(char *buf, size_t cap, size_t *len, const char *s)
98{
99 size_t n = strnlen(s, cap + 1);
100 if (*len + n + 1 > cap)
101 {
102 return false;
103 }
104 memcpy(buf + *len, s, n);
105 *len += n;
106 buf[*len] = '\0';
107 return true;
108}
109
110size_t pc_webdav_xml_escape(char *dst, size_t cap, const char *src)
111{
112 size_t o = 0;
113 if (cap == 0)
114 {
115 return 0;
116 }
117 for (const char *p = src; *p; p++)
118 {
119 const char *rep = nullptr;
120 switch (*p)
121 {
122 case '&':
123 rep = "&amp;";
124 break;
125 case '<':
126 rep = "&lt;";
127 break;
128 case '>':
129 rep = "&gt;";
130 break;
131 case '"':
132 rep = "&quot;";
133 break;
134 case '\'':
135 rep = "&apos;";
136 break;
137 default:
138 break;
139 }
140 if (rep)
141 {
142 size_t rn = strnlen(rep, cap + 1);
143 if (o + rn + 1 > cap)
144 {
145 break;
146 }
147 memcpy(dst + o, rep, rn);
148 o += rn;
149 }
150 else
151 {
152 if (o + 1 + 1 > cap)
153 {
154 break;
155 }
156 dst[o++] = *p;
157 }
158 }
159 dst[o] = '\0';
160 return o;
161}
162
163bool pc_webdav_dest_path(const char *destination, char *out, size_t cap)
164{
165 if (!destination || !out || cap == 0)
166 {
167 return false;
168 }
169
170 // Skip an absolute-URI scheme + authority: after "://", advance to the first
171 // '/' (the path). An abs-path value ("/p/q") is used as-is.
172 const char *p = destination;
173 const char *scheme = strstr(destination, "://");
174 if (scheme)
175 {
176 p = scheme + 3;
177 while (*p && *p != '/')
178 {
179 p++;
180 }
181 if (*p != '/')
182 {
183 return false; // authority with no path
184 }
185 }
186 else if (*p != '/')
187 {
188 return false; // not an absolute path
189 }
190
191 // Percent-decode into out. A while loop so the %XX case can consume its two
192 // extra hex digits without mutating a for-loop counter.
193 size_t o = 0;
194 while (*p)
195 {
196 char c = *p;
197 if (c == '%')
198 {
199 int hi = pc_hex_val(p[1]);
200 int lo = (hi >= 0) ? pc_hex_val(p[2]) : -1;
201 if (hi < 0 || lo < 0)
202 {
203 return false; // malformed escape
204 }
205 c = (char)((hi << 4) | lo);
206 p += 2;
207 }
208 if (o + 1 >= cap)
209 {
210 return false; // no room for char + NUL
211 }
212 out[o++] = c;
213 p++;
214 }
215 out[o] = '\0';
216 return true;
217}
218
219size_t pc_webdav_ms_begin(char *buf, size_t cap, size_t len)
220{
221 app(buf, cap, &len, "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<D:multistatus xmlns:D=\"DAV:\">\n");
222 return len;
223}
224
225size_t pc_webdav_ms_entry(char *buf, size_t cap, size_t len, const char *href, bool is_collection, uint32_t size,
226 const char *rfc1123_mtime, const char *content_type)
227{
228 // Build the whole <response> in a temp first so the append is atomic: a
229 // partial element is never left in the document when the buffer fills.
230 char tmp[512];
231 size_t t = 0;
232 char esc[256];
233
234 pc_webdav_xml_escape(esc, sizeof(esc), href);
235 // The href block is at most 27 + esc(<=255) + 66 == 348 bytes, and adding the collection
236 // marker + resourcetype close reaches <=381 - all well within tmp[512], so these three
237 // atomic-append guards cannot fire (esc is capped by its own 256-byte buffer above).
238 //
239 // GCOVR_EXCL_START buffer-overflow guard, unreachable per the budget above. A block (not
240 // per-line markers) because gcov attributes this multi-line OR-condition's uncovered branch to
241 // the call-opening line of the third app() (the wrapped string-literal argument line), not to
242 // the line carrying the comment - a per-line marker on the wrong line leaves the branch
243 // reported as a real gap.
244 if (!app(tmp, sizeof(tmp), &t, " <D:response>\n <D:href>") || !app(tmp, sizeof(tmp), &t, esc) ||
245 !app(tmp, sizeof(tmp), &t, "</D:href>\n <D:propstat>\n <D:prop>\n <D:resourcetype>"))
246 {
247 return len;
248 }
249 // GCOVR_EXCL_STOP
250
251 if (is_collection && !app(tmp, sizeof(tmp), &t, "<D:collection/>")) // GCOVR_EXCL_BR_LINE app overflow unreachable
252 {
253 return len; // GCOVR_EXCL_LINE unreachable: <=363 < tmp[512] (see above)
254 }
255 if (!app(tmp, sizeof(tmp), &t, "</D:resourcetype>\n")) // GCOVR_EXCL_LINE unreachable: <=381 < tmp[512] (see above)
256 {
257 return len; // GCOVR_EXCL_LINE unreachable: <=381 < tmp[512] (see above)
258 }
259
260 if (!is_collection)
261 {
262 char num[24];
263 unsigned long s = (unsigned long)size;
264 // minimal itoa to avoid pulling in snprintf in the pure core
265 char rev[24];
266 int rn = 0;
267 do
268 {
269 rev[rn++] = (char)('0' + (int)(s % 10));
270 s /= 10;
271 } while (s && rn < (int)sizeof(rev)); // GCOVR_EXCL_BR_LINE unreachable rn-bound: a uint32_t is <=10 digits, rn
272 // never reaches sizeof(rev)==24
273 int ni = 0;
274 while (rn > 0)
275 {
276 num[ni++] = rev[--rn];
277 }
278 num[ni] = '\0';
279 // The href block above tops out at <=381 bytes (<=348 plus the optional
280 // <D:collection/> + </D:resourcetype> close, though this branch only runs for
281 // !is_collection so it's really <=366); this fixed getcontentlength markup + a
282 // 10-digit uint32_t max add <=60 more, so the running total never nears tmp[512]
283 // and these atomic-append guards cannot fire.
284 if (!app(tmp, sizeof(tmp), &t, " <D:getcontentlength>") ||
285 !app(tmp, sizeof(tmp), &t, num) || // GCOVR_EXCL_LINE unreachable: see comment above
286 !app(tmp, sizeof(tmp), &t, "</D:getcontentlength>\n")) // GCOVR_EXCL_LINE unreachable: see comment above
287 {
288 return len; // GCOVR_EXCL_LINE unreachable: see comment above
289 }
290 // content_type block. The append-overflow arm is unreachable per the budget above (running
291 // total <=~446 < tmp[512]); gcov lumps the multi-app OR onto one line, so the whole merged
292 // guard is excluded from coverage rather than carrying a misplaced per-line branch marker.
293 // GCOVR_EXCL_START
294 if (content_type && content_type[0] &&
295 (!app(tmp, sizeof(tmp), &t, " <D:getcontenttype>") || !app(tmp, sizeof(tmp), &t, content_type) ||
296 !app(tmp, sizeof(tmp), &t, "</D:getcontenttype>\n")))
297 {
298 return len;
299 }
300 // GCOVR_EXCL_STOP
301 }
302
303 if (rfc1123_mtime && rfc1123_mtime[0])
304 {
305 if (!app(tmp, sizeof(tmp), &t, " <D:getlastmodified>") || !app(tmp, sizeof(tmp), &t, rfc1123_mtime) ||
306 !app(tmp, sizeof(tmp), &t, "</D:getlastmodified>\n"))
307 {
308 return len;
309 }
310 }
311
312 if (!app(tmp, sizeof(tmp), &t,
313 " </D:prop>\n <D:status>HTTP/1.1 200 OK</D:status>\n"
314 " </D:propstat>\n </D:response>\n"))
315 {
316 return len;
317 }
318
319 // Atomic commit: app() appends the finished element only if it fits and leaves
320 // len unchanged on no-room, so the caller sees an unchanged len and stops adding.
321 app(buf, cap, &len, tmp);
322 return len;
323}
324
325size_t pc_webdav_ms_end(char *buf, size_t cap, size_t len)
326{
327 app(buf, cap, &len, "</D:multistatus>\n");
328 return len;
329}
330
331// True for a byte that ends an XML element name (whitespace, '/', '>').
332static bool name_end_char(char c)
333{
334 // The only caller scans a name span bounded by the '>' index (its tag-end loop stops there),
335 // so this helper never sees '>'; that leg is a defensive, host-unreachable arm. gcov attributes
336 // every operand's branch to this one line, so BR_LINE also drops the (exercised) ws/'/' arms.
337 return c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '/' || c == '>'; // GCOVR_EXCL_BR_LINE
338}
339
340size_t pc_webdav_proppatch_ms(char *buf, size_t cap, const char *href, const char *body, size_t body_len)
341{
342 size_t len = 0;
343 if (cap)
344 {
345 buf[0] = '\0'; // always a valid C-string, even if nothing below fits
346 }
347 char esc[256];
348 pc_webdav_xml_escape(esc, sizeof(esc), href);
349 if (!app(buf, cap, &len,
350 "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<D:multistatus xmlns:D=\"DAV:\">\n"
351 " <D:response>\n <D:href>") ||
352 !app(buf, cap, &len, esc) || !app(buf, cap, &len, "</D:href>\n <D:propstat>\n <D:prop>\n"))
353 {
354 return 0;
355 }
356
357 // Walk the request and echo every element that sits directly inside a <prop>
358 // (across all <set>/<remove> blocks) as a self-closed element. The wrappers
359 // (propertyupdate / set / remove / prop) are skipped; only the properties are
360 // reflected, each refused 403 below.
361 int emitted = 0;
362 bool in_prop = false;
363 size_t i = 0;
364 while (i < body_len && emitted < PC_WEBDAV_MAX_PROPS)
365 {
366 if (body[i] != '<')
367 {
368 i++;
369 continue;
370 }
371 size_t start = i + 1;
372 if (start < body_len && (body[start] == '?' || body[start] == '!'))
373 {
374 while (i < body_len && body[i] != '>') // skip PI / comment / declaration
375 {
376 i++;
377 }
378 i++;
379 continue;
380 }
381 bool closing = (start < body_len && body[start] == '/');
382 if (closing)
383 {
384 start++;
385 }
386 size_t end = start;
387 while (end < body_len && body[end] != '>')
388 {
389 end++;
390 }
391 if (end >= body_len)
392 {
393 break; // unterminated tag
394 }
395 bool self_closed = (end > start && body[end - 1] == '/');
396 size_t name_end = start;
397 while (name_end < end && !name_end_char(body[name_end]))
398 {
399 name_end++;
400 }
401 size_t local = start; // local name = after the last ':' in the qualified name
402 for (size_t k = start; k < name_end; k++)
403 {
404 if (body[k] == ':')
405 {
406 local = k + 1;
407 }
408 }
409 bool is_prop = (name_end - local) == 4 && !strncmp(&body[local], "prop", 4);
410
411 if (closing)
412 {
413 if (is_prop)
414 {
415 in_prop = false;
416 }
417 i = end + 1;
418 continue;
419 }
420 if (is_prop)
421 {
422 if (!self_closed) // <prop> opens a block; <prop/> is an empty block
423 {
424 in_prop = true;
425 }
426 i = end + 1;
427 continue;
428 }
429 if (in_prop)
430 {
431 // Echo this property element self-closed. Copy the open-tag content
432 // (name + its own xmlns/attrs), dropping a trailing '/' and trailing
433 // whitespace; reject a span containing '<' so nothing is injected.
434 size_t copy_end = self_closed ? end - 1 : end;
435 while (copy_end > start && (body[copy_end - 1] == ' ' || body[copy_end - 1] == '\t' ||
436 body[copy_end - 1] == '\r' || body[copy_end - 1] == '\n'))
437 {
438 copy_end--;
439 }
440 bool ok = copy_end > start;
441 for (size_t k = start; k < copy_end && ok; k++)
442 {
443 if (body[k] == '<')
444 {
445 ok = false;
446 }
447 }
448 char tag[256];
449 size_t tl = copy_end - start;
450 if (ok && tl < sizeof(tag))
451 {
452 memcpy(tag, &body[start], tl);
453 tag[tl] = '\0';
454 if (app(buf, cap, &len, " <") && app(buf, cap, &len, tag) && app(buf, cap, &len, "/>\n"))
455 {
456 emitted++;
457 }
458 }
459 if (!self_closed)
460 {
461 // Skip the property's value up to its close tag (no nesting expected).
462 size_t j = end + 1;
463 while (j + 1 < body_len && !(body[j] == '<' && body[j + 1] == '/'))
464 {
465 j++;
466 }
467 while (j < body_len && body[j] != '>')
468 {
469 j++;
470 }
471 i = (j < body_len) ? j + 1 : body_len;
472 }
473 else
474 {
475 i = end + 1;
476 }
477 continue;
478 }
479 i = end + 1;
480 }
481
482 if (!app(buf, cap, &len,
483 " </D:prop>\n <D:status>HTTP/1.1 403 Forbidden</D:status>\n"
484 " </D:propstat>\n </D:response>\n</D:multistatus>\n"))
485 {
486 return 0;
487 }
488 return len;
489}
490
491// ── lock manager (RFC 4918 §6-7) ───────────────────────────────────────────────────────────────
492
493// Copy src into dst[cap], NUL-terminated; false if it does not fit.
494static bool dav_lock_copy(char *dst, size_t cap, const char *src)
495{
496 size_t n = strnlen(src, cap);
497 if (n + 1 > cap)
498 {
499 return false;
500 }
501 memcpy(dst, src, n + 1);
502 return true;
503}
504
505// Normalize a path into dst, stripping trailing '/' (but keeping a lone root "/"). false on overflow.
506static bool dav_lock_norm(char *dst, size_t cap, const char *path)
507{
508 size_t n = strnlen(path, cap);
509 while (n > 1 && path[n - 1] == '/') // drop trailing slashes so "/a/" and "/a" are one resource
510 {
511 n--;
512 }
513 if (n + 1 > cap)
514 {
515 return false;
516 }
517 memcpy(dst, path, n);
518 dst[n] = 0;
519 return true;
520}
521
522// True if `child` equals `parent` or lies (at a segment boundary) under it. Both trailing-slash-normalized.
523static bool dav_lock_same_or_under(const char *parent, const char *child)
524{
525 size_t pn = strnlen(parent, PC_DAV_LOCK_PATH_MAX);
526 if (strncmp(parent, child, pn) != 0)
527 {
528 return false;
529 }
530 if (child[pn] == 0) // exactly equal
531 {
532 return true;
533 }
534 if (pn == 1 && parent[0] == '/') // root covers everything below it
535 {
536 return true;
537 }
538 return child[pn] == '/'; // only a real path-segment boundary, so "/a" does not cover "/ab"
539}
540
541// Do two lock scopes overlap? A Depth-infinity lock's scope is its whole subtree.
542static bool dav_lock_overlap(const char *pa, bool ia, const char *pb, bool ib)
543{
544 if (strcmp(pa, pb) == 0)
545 {
546 return true;
547 }
548 if (ia && dav_lock_same_or_under(pa, pb)) // pb sits under the infinity lock pa
549 {
550 return true;
551 }
552 if (ib && dav_lock_same_or_under(pb, pa)) // pa sits under the infinity lock pb
553 {
554 return true;
555 }
556 return false;
557}
558
559// Does an active lock's scope cover the normalized query path?
560static bool dav_lock_covers(const DavLock *l, const char *np)
561{
562 if (strcmp(l->path, np) == 0)
563 {
564 return true;
565 }
566 return l->depth_infinity && dav_lock_same_or_under(l->path, np);
567}
568
569void pc_dav_lock_init(DavLockTable *t)
570{
571 if (!t)
572 {
573 return;
574 }
575 for (size_t i = 0; i < PC_DAV_LOCK_MAX; i++)
576 {
577 t->locks[i].active = false;
578 }
579}
580
581const DavLock *pc_dav_lock_acquire(DavLockTable *t, const char *path, const char *token, bool exclusive,
582 bool depth_infinity, uint32_t expiry_s)
583{
584 if (!t || !path || !token)
585 {
586 return nullptr;
587 }
588 char np[PC_DAV_LOCK_PATH_MAX];
589 if (!dav_lock_norm(np, sizeof(np), path))
590 {
591 return nullptr;
592 }
593 if (strnlen(token, PC_DAV_LOCK_TOKEN_MAX) + 1 > PC_DAV_LOCK_TOKEN_MAX) // token would not fit
594 {
595 return nullptr;
596 }
597
598 // Conflict: an exclusive request clashes with any overlapping lock; a shared one only with an
599 // overlapping exclusive lock (two shared locks may coexist).
600 for (size_t i = 0; i < PC_DAV_LOCK_MAX; i++)
601 {
602 const DavLock *l = &t->locks[i];
603 if (l->active && dav_lock_overlap(l->path, l->depth_infinity, np, depth_infinity) &&
604 (exclusive || l->exclusive))
605 {
606 return nullptr;
607 }
608 }
609
610 for (size_t i = 0; i < PC_DAV_LOCK_MAX; i++)
611 {
612 DavLock *l = &t->locks[i];
613 if (l->active)
614 {
615 continue;
616 }
617 (void)dav_lock_copy(l->path, sizeof(l->path), np); // np already fits (same cap)
618 (void)dav_lock_copy(l->token, sizeof(l->token), token);
619 l->exclusive = exclusive;
620 l->depth_infinity = depth_infinity;
621 l->expiry_s = expiry_s;
622 l->active = true;
623 return l;
624 }
625 return nullptr; // table full
626}
627
628size_t pc_dav_lock_sweep(DavLockTable *t, uint32_t now_s)
629{
630 if (!t)
631 {
632 return 0;
633 }
634 size_t dropped = 0;
635 for (size_t i = 0; i < PC_DAV_LOCK_MAX; i++)
636 {
637 DavLock *l = &t->locks[i];
638 if (l->active && l->expiry_s != 0 && l->expiry_s <= now_s) // 0 = never expires
639 {
640 l->active = false;
641 dropped++;
642 }
643 }
644 return dropped;
645}
646
647const DavLock *pc_dav_lock_refresh(DavLockTable *t, const char *token, uint32_t new_expiry_s)
648{
649 if (!t || !token)
650 {
651 return nullptr;
652 }
653 for (size_t i = 0; i < PC_DAV_LOCK_MAX; i++)
654 {
655 DavLock *l = &t->locks[i];
656 if (l->active && strcmp(l->token, token) == 0)
657 {
658 l->expiry_s = new_expiry_s;
659 return l;
660 }
661 }
662 return nullptr;
663}
664
665const DavLock *pc_dav_lock_find(const DavLockTable *t, const char *path)
666{
667 if (!t || !path)
668 {
669 return nullptr;
670 }
671 char np[PC_DAV_LOCK_PATH_MAX];
672 if (!dav_lock_norm(np, sizeof(np), path))
673 {
674 return nullptr;
675 }
676 for (size_t i = 0; i < PC_DAV_LOCK_MAX; i++)
677 {
678 if (t->locks[i].active && dav_lock_covers(&t->locks[i], np))
679 {
680 return &t->locks[i];
681 }
682 }
683 return nullptr;
684}
685
686bool pc_dav_lock_release(DavLockTable *t, const char *token)
687{
688 if (!t || !token)
689 {
690 return false;
691 }
692 for (size_t i = 0; i < PC_DAV_LOCK_MAX; i++)
693 {
694 if (t->locks[i].active && strcmp(t->locks[i].token, token) == 0)
695 {
696 t->locks[i].active = false;
697 return true;
698 }
699 }
700 return false;
701}
702
703bool pc_dav_lock_can_write(const DavLockTable *t, const char *path, const char *presented_token)
704{
705 if (!t)
706 {
707 return true; // no table => nothing is locked
708 }
709 if (!path)
710 {
711 return false;
712 }
713 char np[PC_DAV_LOCK_PATH_MAX];
714 if (!dav_lock_norm(np, sizeof(np), path))
715 {
716 return true; // an unparseable path is not something the lock table can guard
717 }
718 bool covered = false;
719 for (size_t i = 0; i < PC_DAV_LOCK_MAX; i++)
720 {
721 const DavLock *l = &t->locks[i];
722 if (!l->active || !dav_lock_covers(l, np))
723 {
724 continue;
725 }
726 covered = true;
727 if (presented_token && strcmp(l->token, presented_token) == 0)
728 {
729 return true; // the request holds a covering lock's token
730 }
731 }
732 return !covered; // unlocked => allowed; locked with no / wrong token => denied
733}
734
735bool pc_dav_if_token(const char *if_header, char *out, size_t cap)
736{
737 if (!if_header || !out || cap == 0)
738 {
739 return false;
740 }
741 // The state tokens live inside a condition list "( ... )"; take the first Coded-URL "<...>" within it
742 // (which also skips the tagged-list resource URL that precedes the '(').
743 const char *lp = strchr(if_header, '(');
744 if (!lp)
745 {
746 return false;
747 }
748 const char *lt = strchr(lp, '<');
749 if (!lt)
750 {
751 return false;
752 }
753 const char *gt = strchr(lt, '>');
754 if (!gt)
755 {
756 return false;
757 }
758 size_t n = (size_t)(gt - lt - 1);
759 if (n + 1 > cap)
760 {
761 return false;
762 }
763 memcpy(out, lt + 1, n);
764 out[n] = 0;
765 return true;
766}
767
768#endif // PC_ENABLE_WEBDAV
Base-16 conversion between raw bytes and their ASCII digits.
int8_t pc_hex_val(char c)
Hex character c as 0..15, or -1 when c is not a hex digit of either case.
Definition hex.h:36
#define PC_WEBDAV_MAX_PROPS
Maximum properties echoed in a WebDAV PROPPATCH 207 response (bounds the response).
WebDAV server core (RFC 4918): method classification, header parsing, and the 207 Multi-Status XML bu...