summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--archive.h49
-rw-r--r--atom.c9
-rw-r--r--concat.c38
-rw-r--r--decode.c14
-rw-r--r--export.c124
-rw-r--r--html.c60
-rw-r--r--template.c35
7 files changed, 181 insertions, 148 deletions
diff --git a/archive.h b/archive.h
index b1cb3dc..532ef47 100644
--- a/archive.h
+++ b/archive.h
@@ -15,7 +15,6 @@
  */
 
 #include <inttypes.h>
-#include <limits.h>
 #include <stdint.h>
 #include <stdio.h>
 #include <stdlib.h>
@@ -108,6 +107,15 @@ static inline bool bodyPartType(
 	return !strcasecmp(partType, type) && !strcasecmp(part->subtype, subtype);
 }
 
+static inline char *paramGet(struct List params, const char *key) {
+	for (size_t i = 0; i + 1 < params.len; i += 2) {
+		if (!strcasecmp(dataCheck(params.ptr[i], String).string, key)) {
+			return dataCheck(params.ptr[i + 1], String).string;
+		}
+	}
+	return NULL;
+}
+
 static inline void bodyPartFree(struct BodyPart part) {
 	if (part.multipart) {
 		for (size_t i = 0; i < part.parts.len; ++i) {
@@ -143,6 +151,7 @@ struct Variable {
 
 typedef int EscapeFn(FILE *file, const char *str);
 
+int escapePath(FILE *file, const char *str);
 int escapeURL(FILE *file, const char *str);
 int escapeXML(FILE *file, const char *str);
 
@@ -160,37 +169,11 @@ char *decodeHeader(const char *header);
 char *decodeToString(const struct BodyPart *part, const char *content);
 int decodeToFile(FILE *file, const struct BodyPart *part, const char *content);
 
-struct Attachment {
-	char path[3][NAME_MAX + 1];
-};
-
-static inline const char *pathUID(uint32_t uid, const char *type) {
-	static char buf[PATH_MAX + 1];
-	snprintf(buf, sizeof(buf), "UID/%" PRIu32 ".%s", uid, type);
-	return buf;
-}
-
-static inline const char *pathSafe(const char *messageID) {
-	if (!strchr(messageID, '/')) return messageID;
-	static char buf[NAME_MAX + 1];
-	strlcpy(buf, messageID, sizeof(buf));
-	for (char *ptr = buf; (ptr = strchr(ptr, '/')); ++ptr) {
-		*ptr = ';';
-	}
-	return buf;
-}
-
-static inline const char *pathMessage(const char *messageID, const char *type) {
-	static char buf[PATH_MAX + 1];
-	snprintf(buf, sizeof(buf), "message/%s.%s", pathSafe(messageID), type);
-	return buf;
-}
-
-static inline const char *pathThread(const char *messageID, const char *type) {
-	static char buf[PATH_MAX + 1];
-	snprintf(buf, sizeof(buf), "thread/%s.%s", pathSafe(messageID), type);
-	return buf;
-}
+#define PATH_UID "UID/[uid].[type]"
+#define PATH_MESSAGE "message/[messageID].[type]"
+#define PATH_THREAD "thread/[messageID].[type]"
+#define PATH_ATTACHMENT \
+	"attachment/[messageID]/[section]/[name][disposition][.][subtype]"
 
 #define MBOX_HEADERS \
 	"Date Subject From Sender Reply-To To Cc Bcc " \
@@ -212,7 +195,7 @@ extern const char *htmlTitle;
 int htmlMessageOpen(FILE *file, const struct Envelope *envelope);
 int htmlInline(FILE *file, const struct BodyPart *part, const char *content);
 int htmlAttachment(
-	FILE *file, const struct BodyPart *part, const struct Attachment *attach
+	FILE *file, const struct BodyPart *part, const struct Variable pathVars[]
 );
 int htmlMessageClose(FILE *file);
 int htmlThreadHead(FILE *file, const struct Envelope *envelope);
diff --git a/atom.c b/atom.c
index 1c4d0de..405bf39 100644
--- a/atom.c
+++ b/atom.c
@@ -51,10 +51,11 @@ static int atomAuthor(FILE *file, struct Address addr) {
 
 static char *atomEntryURL(const struct Envelope *envelope) {
 	struct Variable vars[] = {
-		{ "name", pathSafe(envelope->messageID) },
+		{ "messageID", envelope->messageID },
+		{ "type", "mbox" },
 		{0},
 	};
-	return templateURL("/message/[name].mbox", vars);
+	return templateURL("/" PATH_MESSAGE, vars);
 }
 
 int atomEntryOpen(FILE *file, const struct Envelope *envelope) {
@@ -102,11 +103,11 @@ int atomEntryClose(FILE *file) {
 
 static char *atomFeedURL(const struct Envelope *envelope, const char *type) {
 	struct Variable vars[] = {
-		{ "name", pathSafe(envelope->messageID) },
+		{ "messageID", envelope->messageID },
 		{ "type", type },
 		{0},
 	};
-	return templateURL("/thread/[name].[type]", vars);
+	return templateURL("/" PATH_THREAD, vars);
 }
 
 int atomFeedOpen(FILE *file, const struct Envelope *envelope) {
diff --git a/concat.c b/concat.c
index 94f9bed..a4bf1aa 100644
--- a/concat.c
+++ b/concat.c
@@ -52,10 +52,23 @@ void concatFetch(FILE *imap, enum Atom tag, struct List threads) {
 	fprintf(imap, " (UID ENVELOPE)\r\n");
 }
 
+static const char *uidPath(uint32_t uid, const char *type) {
+	char str[32];
+	snprintf(str, sizeof(str), "%" PRIu32, uid);
+	struct Variable vars[] = {
+		{ "uid", str },
+		{ "type", type },
+		{0},
+	};
+	static char buf[PATH_MAX + 1];
+	templateBuffer(buf, sizeof(buf), PATH_UID, vars, escapePath);
+	return buf;
+}
+
 static time_t uidNewest(struct List uids, const char *type) {
 	time_t newest = 0;
 	for (size_t i = 0; i < uids.len; ++i) {
-		const char *path = pathUID(dataCheck(uids.ptr[i], Number).number, type);
+		const char *path = uidPath(dataCheck(uids.ptr[i], Number).number, type);
 		struct stat status;
 		int error = stat(path, &status);
 		if (error) err(EX_DATAERR, "%s", path);
@@ -86,13 +99,24 @@ static int concatHTML(FILE *file, struct List thread) {
 				|| htmlSubthreadClose(file);
 		} else {
 			uint32_t uid = dataCheck(thread.ptr[i], Number).number;
-			error = concatFile(file, pathUID(uid, "html"));
+			error = concatFile(file, uidPath(uid, "html"));
 		}
 		if (error) return error;
 	}
 	return 0;
 }
 
+static const char *threadPath(const char *messageID, const char *type) {
+	static char buf[PATH_MAX + 1];
+	struct Variable vars[] = {
+		{ "messageID", messageID },
+		{ "type", type },
+		{0},
+	};
+	templateBuffer(buf, sizeof(buf), PATH_THREAD, vars, escapePath);
+	return buf;
+}
+
 const char *concatHead;
 
 void concatData(struct List threads, struct List items) {
@@ -121,7 +145,7 @@ void concatData(struct List threads, struct List items) {
 	const char *path;
 	struct stat status;
 
-	path = pathThread(envelope.messageID, "mbox");
+	path = threadPath(envelope.messageID, "mbox");
 	error = stat(path, &status);
 	if (error || status.st_mtime < uidNewest(flat, "mbox")) {
 		file = fopen(path, "w");
@@ -129,7 +153,7 @@ void concatData(struct List threads, struct List items) {
 
 		for (size_t i = 0; i < flat.len; ++i) {
 			uint32_t uid = dataCheck(flat.ptr[i], Number).number;
-			error = concatFile(file, pathUID(uid, "mbox"));
+			error = concatFile(file, uidPath(uid, "mbox"));
 			if (error) err(EX_IOERR, "%s", path);
 		}
 
@@ -137,7 +161,7 @@ void concatData(struct List threads, struct List items) {
 		if (error) err(EX_IOERR, "%s", path);
 	}
 
-	path = pathThread(envelope.messageID, "atom");
+	path = threadPath(envelope.messageID, "atom");
 	error = stat(path, &status);
 	if (error || status.st_mtime < uidNewest(flat, "atom")) {
 		FILE *file = fopen(path, "w");
@@ -148,7 +172,7 @@ void concatData(struct List threads, struct List items) {
 
 		for (size_t i = 0; i < flat.len; ++i) {
 			uint32_t uid = dataCheck(flat.ptr[i], Number).number;
-			error = concatFile(file, pathUID(uid, "atom"));
+			error = concatFile(file, uidPath(uid, "atom"));
 			if (error) err(EX_IOERR, "%s", path);
 		}
 
@@ -156,7 +180,7 @@ void concatData(struct List threads, struct List items) {
 		if (error) err(EX_IOERR, "%s", path);
 	}
 
-	path = pathThread(envelope.messageID, "html");
+	path = threadPath(envelope.messageID, "html");
 	error = stat(path, &status);
 	if (error || status.st_mtime < uidNewest(flat, "html")) {
 		FILE *file = fopen(path, "w");
diff --git a/decode.c b/decode.c
index 35e6dfd..2604f73 100644
--- a/decode.c
+++ b/decode.c
@@ -244,25 +244,15 @@ char *decodeHeader(const char *header) {
 	return bufferString(&buf);
 }
 
-static const char *partCharset(const struct BodyPart *part) {
-	const char *charset = NULL;
-	for (size_t i = 0; i + 1 < part->params.len; i += 2) {
-		const char *key = dataCheck(part->params.ptr[i], String).string;
-		if (strcasecmp(key, "charset")) continue;
-		charset = dataCheck(part->params.ptr[i + 1], String).string;
-	}
-	return charset;
-}
-
 char *decodeToString(const struct BodyPart *part, const char *src) {
 	struct Buffer dst = bufferAlloc(strlen(src) + 1);
-	decode(&dst, part->encoding, partCharset(part), src);
+	decode(&dst, part->encoding, paramGet(part->params, "charset"), src);
 	return bufferString(&dst);
 }
 
 int decodeToFile(FILE *file, const struct BodyPart *part, const char *src) {
 	struct Buffer dst = bufferAlloc(strlen(src));
-	decode(&dst, part->encoding, partCharset(part), src);
+	decode(&dst, part->encoding, paramGet(part->params, "charset"), src);
 	size_t n = fwrite(dst.ptr, dst.len, 1, file);
 	free(dst.ptr);
 	return (n ? 0 : -1);
diff --git a/export.c b/export.c
index 9aa3126..873870c 100644
--- a/export.c
+++ b/export.c
@@ -29,15 +29,28 @@
 #include "archive.h"
 #include "imap.h"
 
+static const char *exportPath(uint32_t uid, const char *type) {
+	char str[32];
+	snprintf(str, sizeof(str), "%" PRIu32, uid);
+	struct Variable vars[] = {
+		{ "uid", str },
+		{ "type", type },
+		{0},
+	};
+	static char buf[PATH_MAX + 1];
+	templateBuffer(buf, sizeof(buf), PATH_UID, vars, escapePath);
+	return buf;
+}
+
 bool exportFetch(FILE *imap, enum Atom tag, struct List threads) {
 	struct List uids = {0};
 	listFlatten(&uids, threads);
 	for (size_t i = uids.len - 1; i < uids.len; --i) {
 		uint32_t uid = dataCheck(uids.ptr[i], Number).number;
 		int error = 0
-			|| access(pathUID(uid, "atom"), F_OK)
-			|| access(pathUID(uid, "html"), F_OK)
-			|| access(pathUID(uid, "mbox"), F_OK);
+			|| access(exportPath(uid, "atom"), F_OK)
+			|| access(exportPath(uid, "html"), F_OK)
+			|| access(exportPath(uid, "mbox"), F_OK);
 		if (!error) uids.ptr[i] = uids.ptr[--uids.len];
 	}
 	if (!uids.len) {
@@ -61,7 +74,7 @@ static void exportMbox(
 	uint32_t uid, const struct Envelope *envelope,
 	const char *header, const char *body
 ) {
-	const char *path = pathUID(uid, "mbox");
+	const char *path = exportPath(uid, "mbox");
 	FILE *file = fopen(path, "w");
 	if (!file) err(EX_CANTCREAT, "%s", path);
 	int error = 0
@@ -71,10 +84,17 @@ static void exportMbox(
 		|| fclose(file);
 	if (error) err(EX_IOERR, "%s", path);
 
-	const char *msg = pathMessage(envelope->messageID, "mbox");
-	unlink(msg);
-	error = link(path, msg);
-	if (error) err(EX_CANTCREAT, "%s", msg);
+	char buf[PATH_MAX + 1];
+	struct Variable vars[] = {
+		{ "messageID", envelope->messageID },
+		{ "type", "mbox" },
+		{0},
+	};
+	templateBuffer(buf, sizeof(buf), PATH_MESSAGE, vars, escapePath);
+
+	unlink(buf);
+	error = link(path, buf);
+	if (error) err(EX_CANTCREAT, "%s", buf);
 }
 
 static bool isInline(const struct BodyPart *part) {
@@ -87,7 +107,7 @@ static void exportAtom(
 	uint32_t uid, const struct Envelope *envelope,
 	const struct BodyPart *structure, struct Data body
 ) {
-	const char *path = pathUID(uid, "atom");
+	const char *path = exportPath(uid, "atom");
 	FILE *file = fopen(path, "w");
 	if (!file) err(EX_CANTCREAT, "%s", path);
 
@@ -118,61 +138,55 @@ static void exportAtom(
 	if (error) err(EX_IOERR, "%s", path);
 }
 
-static struct Attachment exportAttachment(
-	const struct Envelope *envelope, struct List section,
+static int exportHTMLAttachment(
+	FILE *file, const struct Envelope *envelope, struct List *section,
 	const struct BodyPart *part, struct Data body
 ) {
-	struct Attachment attach = { "", "", "" };
-	strlcpy(
-		attach.path[0], pathSafe(envelope->messageID), sizeof(attach.path[0])
-	);
-	for (size_t i = 0; i < section.len; ++i) {
-		uint32_t num = dataCheck(section.ptr[i], Number).number;
-		char buf[32];
-		snprintf(buf, sizeof(buf), "%s%" PRIu32, (i ? "." : ""), num);
-		strlcat(attach.path[1], buf, sizeof(attach.path[1]));
-	}
-	struct List params = part->disposition.params;
-	for (size_t i = 0; i + 1 < params.len; i += 2) {
-		const char *key = dataCheck(params.ptr[i], String).string;
-		if (strcasecmp(key, "filename")) continue;
-		const char *value = dataCheck(params.ptr[i + 1], String).string;
-		strlcpy(attach.path[2], pathSafe(value), sizeof(attach.path[2]));
-	}
-	if (!attach.path[2][0]) {
-		const char *disposition = part->disposition.type;
-		if (!disposition) disposition = "INLINE";
-		strlcat(attach.path[2], pathSafe(disposition), sizeof(attach.path[2]));
-		strlcat(attach.path[2], ".", sizeof(attach.path[2]));
-		strlcat(attach.path[2], pathSafe(part->subtype), sizeof(attach.path[2]));
+	char buf[256] = "";
+	for (size_t i = 0; i < section->len; ++i) {
+		snprintf(
+			&buf[strlen(buf)], sizeof(buf) - strlen(buf), "%s%" PRIu32,
+			(i ? "." : ""), dataCheck(section->ptr[i], Number).number
+		);
 	}
-
-	char path[PATH_MAX + 1] = "attachment";
-	for (int i = 0; i < 2; ++i) {
-		strlcat(path, "/", sizeof(path));
-		strlcat(path, attach.path[i], sizeof(path));
+	const char *name = paramGet(part->disposition.params, "filename");
+	const char *disposition = part->disposition.type;
+	if (!disposition) disposition = "INLINE";
+
+	char path[PATH_MAX + 1];
+	struct Variable vars[] = {
+		{ "messageID", envelope->messageID },
+		{ "section", buf },
+		{ "name", (name ? name : "") },
+		{ "disposition", (name ? "" : disposition) },
+		{ ".", (name ? "" : ".") },
+		{ "subtype", (name ? "" : part->subtype) },
+		{0},
+	};
+	templateBuffer(path, sizeof(path), PATH_ATTACHMENT, vars, escapePath);
+
+	for (char *ch = path; (ch = strchr(ch, '/')); ++ch) {
+		*ch = '\0';
 		int error = mkdir(path, 0775);
 		if (error && errno != EEXIST) err(EX_CANTCREAT, "%s", path);
+		*ch = '/';
 	}
-	strlcat(path, "/", sizeof(path));
-	strlcat(path, attach.path[2], sizeof(path));
 
-	FILE *file = fopen(path, "w");
+	FILE *attachment = fopen(path, "w");
 	if (!file) err(EX_CANTCREAT, "%s", path);
 
 	int error = 0
-		|| decodeToFile(file, part, dataCheck(body, String).string)
-		|| fclose(file);
+		|| decodeToFile(attachment, part, dataCheck(body, String).string)
+		|| fclose(attachment);
 	if (error) err(EX_IOERR, "%s", path);
 
-	return attach;
+	return htmlAttachment(file, part, vars);
 }
 
 static int exportHTMLBody(
 	FILE *file, const struct Envelope *envelope, struct List *section,
 	const struct BodyPart *part, struct Data body
 ) {
-	int error = 0;
 	if (bodyPartType(part, "multipart", "alternative")) {
 		for (size_t i = part->parts.len - 1; i < part->parts.len; --i) {
 			if (!isInline(&part->parts.ptr[i])) continue;
@@ -191,41 +205,39 @@ static int exportHTMLBody(
 		for (size_t i = 0; i < part->parts.len; ++i) {
 			struct Data num = { .type = Number, .number = 1 + i };
 			listPush(section, num);
-			error = exportHTMLBody(
+			int error = exportHTMLBody(
 				file, envelope, section,
 				&part->parts.ptr[i], dataCheck(body, List).list.ptr[i]
 			);
 			if (error) return error;
 			section->len--;
 		}
+		return 0;
 
 	} else if (part->message.structure) {
 		const struct BodyPart *structure = part->message.structure;
-		error = 0
+		int error = 0
 			|| htmlMessageOpen(file, part->message.envelope)
 			|| exportHTMLBody(file, envelope, section, structure, body)
 			|| htmlMessageClose(file);
+		return error;
 
 	} else if (isInline(part)) {
 		char *content = decodeToString(part, dataCheck(body, String).string);
-		error = htmlInline(file, part, content);
+		int error = htmlInline(file, part, content);
 		free(content);
+		return error;
 
 	} else {
-		// TODO: Open and close attachment lists.
-		struct Attachment attach = exportAttachment(
-			envelope, *section, part, body
-		);
-		error = htmlAttachment(file, part, &attach);
+		return exportHTMLAttachment(file, envelope, section, part, body);
 	}
-	return error;
 }
 
 static void exportHTML(
 	uint32_t uid, const struct Envelope *envelope,
 	const struct BodyPart *structure, struct Data body
 ) {
-	const char *path = pathUID(uid, "html");
+	const char *path = exportPath(uid, "html");
 	FILE *file = fopen(path, "w");
 	if (!file) err(EX_CANTCREAT, "%s", path);
 
diff --git a/html.c b/html.c
index 2d24ce2..403caef 100644
--- a/html.c
+++ b/html.c
@@ -94,10 +94,11 @@ static char *htmlFragment(const struct Envelope *envelope) {
 
 static char *htmlMbox(const struct Envelope *envelope) {
 	struct Variable vars[] = {
-		{ "name", pathSafe(envelope->messageID) },
+		{ "messageID", envelope->messageID },
+		{ "type", "mbox" },
 		{0},
 	};
-	return templateURL("../message/[name].mbox", vars);
+	return templateURL("../" PATH_MESSAGE, vars);
 }
 
 int htmlMessageOpen(FILE *file, const struct Envelope *envelope) {
@@ -164,26 +165,20 @@ int htmlInline(FILE *file, const struct BodyPart *part, const char *content) {
 	return templateRender(file, template, vars, escapeXML);
 }
 
-static char *htmlAttachmentURL(const struct Attachment *attach) {
-	struct Variable vars[] = {
-		{ "path0", attach->path[0] },
-		{ "path1", attach->path[1] },
-		{ "path2", attach->path[2] },
-		{0},
-	};
-	return templateURL("../attachment/[path0]/[path1]/[path2]", vars);
-}
-
 int htmlAttachment(
-	FILE *file, const struct BodyPart *part, const struct Attachment *attach
+	FILE *file, const struct BodyPart *part, const struct Variable *path
 ) {
 	const char *template = TEMPLATE(
-		<a class="attachment" href="[url]">[name]</a>
+		<a class="attachment" href="[url]">[name][type][/][subtype]</a>
 	);
-	char *url = htmlAttachmentURL(attach);
+	char *url = templateURL("../" PATH_ATTACHMENT, path);
+	const char *name = paramGet(part->disposition.params, "filename");
 	struct Variable vars[] = {
 		{ "url", url },
-		{ "name", attach->path[2] }, // FIXME: Show intended name or type.
+		{ "name", (name ? name : "") },
+		{ "type", (name ? "" : part->type) },
+		{ "/", (name ? "" : "/") },
+		{ "subtype", (name ? "" : part->subtype) },
 		{0},
 	};
 	int error = templateRender(file, template, vars, escapeXML);
@@ -197,12 +192,13 @@ int htmlMessageClose(FILE *file) {
 
 const char *htmlTitle;
 
-static char *htmlThreadURL(const struct Envelope *envelope) {
+static char *htmlThreadURL(const struct Envelope *envelope, const char *type) {
 	struct Variable vars[] = {
-		{ "name", pathSafe(envelope->messageID) },
+		{ "messageID", envelope->messageID },
+		{ "type", type },
 		{0},
 	};
-	return templateURL("[name]", vars);
+	return templateURL("../" PATH_THREAD, vars);
 }
 
 int htmlThreadHead(FILE *file, const struct Envelope *envelope) {
@@ -210,18 +206,21 @@ int htmlThreadHead(FILE *file, const struct Envelope *envelope) {
 		<!DOCTYPE html>
 		<meta charset="utf-8">
 		<title>[subject] &middot; [title]</title>
-		<link rel="alternate" type="application/atom+xml" href="[url].atom">
-		<link rel="alternate" type="application/mbox" href="[url].mbox">
+		<link rel="alternate" type="application/atom+xml" href="[atom]">
+		<link rel="alternate" type="application/mbox" href="[mbox]">
 	);
-	char *url = htmlThreadURL(envelope);
+	char *atom = htmlThreadURL(envelope, "atom");
+	char *mbox = htmlThreadURL(envelope, "mbox");
 	struct Variable vars[] = {
 		{ "subject", envelope->subject },
 		{ "title", htmlTitle },
-		{ "url", url },
+		{ "atom", atom },
+		{ "mbox", mbox },
 		{0},
 	};
 	int error = templateRender(file, template, vars, escapeXML);
-	free(url);
+	free(atom);
+	free(mbox);
 	return error;
 }
 
@@ -231,21 +230,24 @@ int htmlThreadOpen(FILE *file, const struct Envelope *envelope) {
 			<h1>[subject]</h1>
 			<nav>
 				<ul>
-					<li><a href="[url].atom">follow</a></li>
-					<li><a href="[url].mbox">download</a></li>
+					<li><a href="[atom]">follow</a></li>
+					<li><a href="[mbox]">download</a></li>
 				</ul>
 			</nav>
 		</header>
 		<main class="thread">
 	);
-	char *url = htmlThreadURL(envelope);
+	char *atom = htmlThreadURL(envelope, "atom");
+	char *mbox = htmlThreadURL(envelope, "mbox");
 	struct Variable vars[] = {
 		{ "subject", envelope->subject },
-		{ "url", url },
+		{ "atom", atom },
+		{ "mbox", mbox },
 		{0},
 	};
 	int error = templateRender(file, template, vars, escapeXML);
-	free(url);
+	free(atom);
+	free(mbox);
 	return error;
 }
 
diff --git a/template.c b/template.c
index afb9173..4f2d9dd 100644
--- a/template.c
+++ b/template.c
@@ -29,6 +29,25 @@ static int escapeNull(FILE *file, const char *str) {
 	return (n ? 0 : -1);
 }
 
+static const char SlashReplacement = ';';
+
+int escapePath(FILE *file, const char *str) {
+	while (*str) {
+		if (*str == '/') {
+			str++;
+			int n = fprintf(file, "%c", SlashReplacement);
+			if (n < 0) return n;
+		}
+		size_t len = strcspn(str, "/");
+		if (len) {
+			size_t n = fwrite(str, len, 1, file);
+			if (!n) return -1;
+		}
+		str += len;
+	}
+	return 0;
+}
+
 int escapeURL(FILE *file, const char *str) {
 	static const char *Safe = {
 		"$-_.+!*'(),"
@@ -44,7 +63,9 @@ int escapeURL(FILE *file, const char *str) {
 		}
 		str += len;
 		if (*str) {
-			int n = fprintf(file, "%%%02X", *str++);
+			char ch = *str++;
+			if (ch == '/') ch = SlashReplacement;
+			int n = fprintf(file, "%%%02X", ch);
 			if (n < 0) return n;
 		}
 	}
@@ -53,12 +74,6 @@ int escapeURL(FILE *file, const char *str) {
 
 int escapeXML(FILE *file, const char *str) {
 	while (*str) {
-		size_t len = strcspn(str, "\"&<");
-		if (len) {
-			size_t n = fwrite(str, len, 1, file);
-			if (!n) return -1;
-		}
-		str += len;
 		int n = 0;
 		switch (*str) {
 			break; case '"': str++; n = fprintf(file, "&quot;");
@@ -66,6 +81,12 @@ int escapeXML(FILE *file, const char *str) {
 			break; case '<': str++; n = fprintf(file, "&lt;");
 		}
 		if (n < 0) return n;
+		size_t len = strcspn(str, "\"&<");
+		if (len) {
+			size_t n = fwrite(str, len, 1, file);
+			if (!n) return -1;
+		}
+		str += len;
 	}
 	return 0;
 }