~sircmpwn/hare-message

7dab49fefd4e141dfbbc194bc93e7f183cee9e1e — Drew DeVault 8 months ago 2f3f361
message/header: implement header encoding/raw ops
2 files changed, 266 insertions(+), 8 deletions(-)

M COPYING
M message/header.ha
M COPYING => COPYING +24 -0
@@ 1,3 1,27 @@
Parts of hare-message are based on Simon Ser's go-message

MIT License

Copyright (c) 2016 emersion

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Mozilla Public License Version 2.0
==================================


M message/header.ha => message/header.ha +242 -8
@@ 1,5 1,13 @@
use ascii;
use bufio;
use bytes;
use errors;
use fmt;
use hash::fnv;
use io;
use strings;
use strio;
use types;

export def HEADER_BUCKETS: u64 = 16;



@@ 25,6 33,37 @@ fn header_field_destroy(hf: *header_field) void = {
	free(hf);
};

// Returns the raw representation of this header field, including CRLF. The
// return value is borrowed from the header field.
fn header_field_raw(hf: *header_field) ([]u8 | errors::invalid) = {
	if (len(hf.raw) != 0) {
		return hf.raw;
	};

	const iter = strings::iter(hf.key);
	for (true) {
		const rn = match (strings::next(&iter)) {
		case let rn: rune =>
			yield rn;
		case void =>
			break;
		};

		if (!ascii::isprint(rn) || rn == ':') {
			return errors::invalid;
		};
	};

	if (strings::contains(hf.val, "\r\n")) {
		return errors::invalid;
	};

	let sink = bufio::dynamic(io::mode::WRITE);
	header_field_fmt(&sink, hf)!;
	hf.raw = bufio::buffer(&sink);
	return hf.raw;
};

export type header = struct {
	fields: []*header_field,
	map: [HEADER_BUCKETS][]header_map_key,


@@ 57,11 96,7 @@ export fn header_finish(head: *header) void = {
	};
};

// Adds a key, value pair to a [[header]].
export fn header_add(head: *header, key: str, val: str) void = {
	const key = canonical_mime_header_key(key);
	defer free(key);

fn header_get_mapkey(head: *header, key: str) *header_map_key = {
	const i = fnv::string64(key) % HEADER_BUCKETS;
	const bucket = &head.map[i];



@@ 73,16 108,23 @@ export fn header_add(head: *header, key: str, val: str) void = {
		};
	};

	let map = match (map) {
	match (map) {
	case let map: *header_map_key =>
		yield map;
		return map;
	case null =>
		append(bucket, header_map_key {
			key = strings::dup(key),
			...
		});
		yield &bucket[len(bucket) - 1];
		return &bucket[len(bucket) - 1];
	};
};

// Adds a key, value pair to a [[header]].
export fn header_add(head: *header, key: str, val: str) void = {
	const key = canonical_mime_header_key(key);
	defer free(key);
	let map = header_get_mapkey(head, key);

	const field = alloc(new_header_field(key, val, []));
	append(head.fields, field);


@@ 126,6 168,70 @@ export fn header_get(head: *header, key: str) str = {
	assert(header_get(&head, "foobar") == "");
};

// Adds a raw key, value pair to a header. The slice should include the complete
// header field including key, value, colon, and CRLF. If the input is not a
// valid MIME header, this function aborts.
export fn header_add_raw(head: *header, kv: []u8) void = {
	const colon = bytes::index(kv, ':') as size;
	const key = bytes::trim(kv[..colon], ' ', '\t');
	const key = canonical_mime_header_key(strings::fromutf8(key)!);
	let map = header_get_mapkey(head, key);
	const val = decode_header_value(kv[colon..]);

	const field = alloc(new_header_field(key, val, alloc(kv...)));
	append(head.fields, field);
	append(map.fields, field);
};

// Gets the first raw header field associated with a header key, or returns []
// if unset. The return value is borrowed from the header.
export fn header_get_raw(head: *header, key: str) ([]u8 | errors::invalid) = {
	const key = canonical_mime_header_key(key);
	defer free(key);

	const i = fnv::string64(key) % HEADER_BUCKETS;
	const bucket = &head.map[i];
	for (let i = 0z; i < len(bucket); i += 1) {
		const map = &bucket[i];
		if (map.key != key) {
			continue;
		};
		return header_field_raw(map.fields[len(map.fields) - 1])?;
	};

	return [];
};

@test fn header_add_raw() void = {
	const head = new_header();
	defer header_finish(&head);

	const dkim = "DKIM-Signature: a=rsa-sha256; bh=uI/rVH7mLBSWkJVvQYKz3TbpdI2BLZWTIMKcuo0KHOI=; c=simple/simple; d=example.org; h=Subject:To:From; s=default; t=1577562184; v=1; b=;\r\n";
	header_add_raw(&head, strings::toutf8(dkim));

	const raw = header_get_raw(&head, "DKIM-SIGNature")!;
	const raw = strings::fromutf8(raw)!;
	assert(raw == dkim);
};

@test fn header_get_raw() void = {
	const head = new_header();
	defer header_finish(&head);

	header_add(&head, "content-type", "text/plain");
	header_add(&head, "DKIM-Signature", "a=rsa-sha256; bh=uI/rVH7mLBSWkJVvQYKz3TbpdI2BLZWTIMKcuo0KHOI=; c=simple/simple; d=example.org; h=Subject:To:From; s=default; t=1577562184; v=1; b=;");

	const raw = header_get_raw(&head, "Content-Type")!;
	const raw = strings::fromutf8(raw)!;
	assert(raw == "Content-Type: text/plain\r\n");

	const raw = header_get_raw(&head, "DKIM-Signature")!;
	const raw = strings::fromutf8(raw)!;
	assert(raw == "Dkim-Signature: a=rsa-sha256;\r\n"
		" bh=uI/rVH7mLBSWkJVvQYKz3TbpdI2BLZWTIMKcuo0KHOI=; c=simple/simple;\r\n"
		" d=example.org; h=Subject:To:From; s=default; t=1577562184; v=1; b=;\r\n");
};

// Returns all of the header values associated with a given key. The returned
// slice must be freed by the caller, but the strings within it are borrowed
// from the header -- use free(), not [[strings::freeall]].


@@ 233,3 339,131 @@ export fn header_set(head: *header, key: str, val: str) void = {
	assert(len(vals) == 1);
	assert(vals[0] == "text/x-hare");
};

def PREFERRED_HEADER_LEN = 76z;
def MAX_HEADER_LEN = 998z;

// Formats a header field, ensuring each line is no longer than 76 characters,
// folding at whitespace if possible. If a word exceeds this limit, it will be
// split.
fn header_field_fmt(sink: io::handle, hf: *header_field) (size | io::error) = {
	let z = fmt::fprintf(sink, "{}: ", hf.key)?;
	if (hf.val == "") {
		z += fmt::fprint(sink, "\r\n")?;
		return z;
	};

	let first = true;
	let val = strings::toutf8(strings::dup(hf.val));
	for (len(val) > 0) {
		let keylen = 0z;
		if (first) {
			keylen = z;
		};

		let (l, next, ok) = fold_line(val, PREFERRED_HEADER_LEN - keylen);
		if (!ok) {
			let v = fold_line(val, MAX_HEADER_LEN - keylen);
			l = v.0; next = v.1;
		};
		io::write(sink, l)?;
		free(l);
		free(val);
		val = next;
		first = false;
	};

	return z;
};

fn fold_line(v: []u8, max: size) ([]u8, []u8, bool) = {
	let ok = true;

	let fold_before = max + 1;
	let fold_at = len(v);

	let folding = "";
	if (fold_before > len(v)) {
		// End of string
		if (v[len(v)-1] != '\n') {
			// Add CRLF if not already present
			folding = "\r\n";
		};
	} else {
		const needles: [_]u8 = [' ', '\t', '\n'];
		fold_at = types::SIZE_MAX;
		for (let i = 0z; i < len(needles); i += 1) {
			match (bytes::rindex(v[..fold_before], needles[i])) {
			case let z: size =>
				if (fold_at == types::SIZE_MAX || z > fold_at) {
					fold_at = z;
				};
			case void =>
				yield;
			};
		};

		if (fold_at == 0) {
			// Whitespace at the previous folding whitespace
			fold_at = fold_before - 1;
		} else if (fold_at == types::SIZE_MAX) {
			// Insert whitespace
			fold_at = fold_before - 2;
		};

		assert(fold_at < len(v));
		switch (v[fold_at]) {
		case ' ', '\t' =>
			if (v[fold_at - 1] != '\n') {
				// Next char is whitespace, no need to add one
				folding = "\r\n";
			};
		case '\n' =>
			// Already CRLF, nothing to do
			folding = "";
		case =>
			// Another char is present, so add a continuation line.
			// This inserts an extra space in the string and should
			// be avoided if possible.
			folding = "\r\n ";
			ok = false;
		};
	};

	let line = alloc(v[..fold_at]...);
	append(line, strings::toutf8(folding)...);
	let next = alloc(v[fold_at..]...);
	return (line, next, ok);
};

// Decodes a header value, collapsing continuation lines as necessary. The
// caller must free the return value.
fn decode_header_value(v: []u8) str = {
	const sink = strio::dynamic();
	for (true) {
		const i = match (bytes::index(v, '\n')) {
		case void =>
			write_continued(&sink, v);
			break;
		case let z: size =>
			yield z;
		};
		write_continued(&sink, v[..i]);
		v = v[i+1..];
	};
	return strio::string(&sink);
};

fn write_continued(sink: *strio::stream, v: []u8) void = {
	if (len(v) > 0 && v[len(v)-1] == '\r') {
		v = v[..len(v)-1];
	};
	v = bytes::trim(v, ' ', '\t');
	if (len(v) == 0) {
		return;
	};
	if (len(strio::string(sink)) != 0) {
		strio::appendrune(sink, ' ')!;
	};
	io::write(sink, v)!;
};