Commit Diff


commit - bd4ce114c04c569958acb4d8c7ecb00465221fb3
commit + 79f05bf96cd7e01f3482670fd236c624bba177e0
blob - 1090a5bf0d5dc1f8c9e76e79baba43a936f5db9a
blob + dfeb75234cd47546b69f831bdd53ad9733c02a09
--- regress/artifact_result/artifact_result_test.c
+++ regress/artifact_result/artifact_result_test.c
@@ -24,6 +24,8 @@
 
 static const char good_id[] = "0123456789abcdef0123456789abcdef";
 
+static void streamed_matches_contiguous(const void *, size_t, size_t);
+
 static int
 field_count(const struct json *j, int obj, const char *name)
 {
@@ -74,7 +76,164 @@ string_field(const struct json *j, int root, const cha
 	return (json_get_str(j, json_obj_get(j, root, name), len));
 }
 
+static int
+base64_value(u_char c)
+{
+	if (c >= 'A' && c <= 'Z')
+		return (c - 'A');
+	if (c >= 'a' && c <= 'z')
+		return (c - 'a' + 26);
+	if (c >= '0' && c <= '9')
+		return (c - '0' + 52);
+	if (c == '+')
+		return (62);
+	if (c == '/')
+		return (63);
+	return (-1);
+}
+
+static int
+base64_decode(struct buf *out, const char *src, size_t len)
+{
+	uint32_t v;
+	int a, b, c, d;
+	size_t i;
+
+	buf_reset(out);
+	if (len % 4 != 0)
+		return (-1);
+	for (i = 0; i < len; i += 4) {
+		a = base64_value((u_char)src[i]);
+		b = base64_value((u_char)src[i + 1]);
+		c = src[i + 2] == '=' ? -2 :
+		    base64_value((u_char)src[i + 2]);
+		d = src[i + 3] == '=' ? -2 :
+		    base64_value((u_char)src[i + 3]);
+		if (a < 0 || b < 0 || c == -1 || d == -1 ||
+		    (c == -2 && d != -2) ||
+		    ((c == -2 || d == -2) && i + 4 != len))
+			return (-1);
+		v = ((uint32_t)a << 18) | ((uint32_t)b << 12);
+		if (c >= 0)
+			v |= (uint32_t)c << 6;
+		if (d >= 0)
+			v |= (uint32_t)d;
+		buf_addc(out, (u_char)(v >> 16));
+		if (c >= 0)
+			buf_addc(out, (u_char)(v >> 8));
+		if (d >= 0)
+			buf_addc(out, (u_char)v);
+	}
+	return (0);
+}
+
+static size_t
+encoded_field_len(const struct buf *out, const char *name)
+{
+	char pattern[64];
+	const u_char *p = out->data;
+	size_t i, j, n, start;
+	int written;
+
+	written = snprintf(pattern, sizeof(pattern), "\"%s\":\"", name);
+	if (written < 0 || (size_t)written >= sizeof(pattern))
+		return (SIZE_MAX);
+	n = (size_t)written;
+	for (i = 0; i + n <= out->len; i++) {
+		if (memcmp(p + i, pattern, n) != 0)
+			continue;
+		start = i + n;
+		for (j = start; j < out->len;) {
+			if (p[j] == '"')
+				return (j - start);
+			if (p[j] == '\\') {
+				if (j + 1 >= out->len)
+					return (SIZE_MAX);
+				j += 2;
+			} else
+				j++;
+		}
+		return (SIZE_MAX);
+	}
+	return (SIZE_MAX);
+}
+
+static int
+preview_bytes(const struct json *j, int root, const char *field,
+    const char *encoding, struct buf *decoded)
+{
+	char *value;
+	size_t len;
+	int rc = 0;
+
+	value = string_field(j, root, field, &len);
+	if (value == NULL)
+		return (-1);
+	buf_reset(decoded);
+	if (strcmp(encoding, "base64") == 0)
+		rc = base64_decode(decoded, value, len);
+	else if (strcmp(encoding, "utf-8") == 0)
+		buf_add(decoded, value, len);
+	else
+		rc = -1;
+	free(value);
+	return (rc);
+}
+
 static void
+check_preview_result(const struct buf *out, const u_char *raw, size_t raw_len,
+    size_t head_raw_len, size_t tail_raw_len, const char *want_encoding,
+    int abbreviated)
+{
+	struct json j;
+	struct buf head, tail;
+	char *encoding, *id;
+	size_t elen, head_encoded, idlen, tail_encoded;
+	int root;
+
+	CHECK(out->len <= ARTIFACT_RESULT_MAX);
+	CHECK(json_parse(&j, out->data, out->len, 0) == 0);
+	root = json_root(&j);
+	CHECK(json_get_bool(&j, json_obj_get(&j, root,
+	    "preview_abbreviated")) == abbreviated);
+	CHECK(field_count(&j, root, "id") == abbreviated);
+	if (abbreviated) {
+		id = string_field(&j, root, "id", &idlen);
+		CHECK(id != NULL && idlen == sizeof(good_id) - 1 &&
+		    memcmp(id, good_id, idlen) == 0);
+		free(id);
+	} else
+		CHECK(json_obj_get(&j, root, "id") == -1);
+	encoding = string_field(&j, root, "preview_encoding", &elen);
+	CHECK(encoding != NULL && elen == strlen(want_encoding) &&
+	    memcmp(encoding, want_encoding, elen) == 0);
+	head_encoded = encoded_field_len(out, "preview_head");
+	tail_encoded = encoded_field_len(out, "preview_tail");
+	CHECK(head_encoded <= ARTIFACT_PREVIEW_HEAD_ENCODED_MAX);
+	CHECK(tail_encoded <= ARTIFACT_PREVIEW_TAIL_ENCODED_MAX);
+	CHECK(head_encoded != SIZE_MAX && tail_encoded != SIZE_MAX &&
+	    out->len >= head_encoded + tail_encoded &&
+	    out->len - head_encoded - tail_encoded <=
+	    ARTIFACT_RESULT_META_RESERVE);
+	buf_init(&head);
+	buf_init(&tail);
+	CHECK(encoding != NULL &&
+	    preview_bytes(&j, root, "preview_head", want_encoding, &head) == 0);
+	CHECK(encoding != NULL &&
+	    preview_bytes(&j, root, "preview_tail", want_encoding, &tail) == 0);
+	CHECK(head.len == head_raw_len && tail.len == tail_raw_len);
+	CHECK(head.len <= raw_len && tail.len <= raw_len - head.len &&
+	    memcmp(head.data, raw, head.len) == 0 &&
+	    memcmp(tail.data, raw + raw_len - tail.len, tail.len) == 0);
+	if (!abbreviated)
+		CHECK(head.len + tail.len == raw_len);
+	buf_free(&tail);
+	buf_free(&head);
+	free(encoding);
+	json_done(&j);
+}
+
+static void
 test_inline_and_forgery(void)
 {
 	static const char *const keys[] = {
@@ -228,22 +387,30 @@ test_exact_inline_boundaries(void)
 {
 	static const struct {
 		const char *name;
-		u_char byte;
+		u_char head_byte;
+		u_char marker_byte;
+		u_char tail_byte;
 		const char *encoding;
-		size_t encoded_step;
+		size_t head_raw_max;
+		size_t tail_raw_max;
 	} shapes[] = {
-		{ "printable", 'x', "utf-8", 1 },
-		{ "escape-heavy", '\1', "utf-8", 6 },
-		{ "NUL", '\0', "utf-8", 6 },
-		{ "invalid UTF-8/base64", 0xff, "base64", 4 }
+		{ "printable", 'h', 'm', 't', "utf-8",
+		    ARTIFACT_PREVIEW_HEAD_ENCODED_MAX,
+		    ARTIFACT_PREVIEW_TAIL_ENCODED_MAX },
+		{ "escape-heavy", '\1', '\2', '\3', "utf-8",
+		    ARTIFACT_PREVIEW_HEAD_ENCODED_MAX / 6,
+		    ARTIFACT_PREVIEW_TAIL_ENCODED_MAX / 6 },
+		{ "NUL", '\0', '\1', '\37', "utf-8",
+		    ARTIFACT_PREVIEW_HEAD_ENCODED_MAX / 6,
+		    ARTIFACT_PREVIEW_TAIL_ENCODED_MAX / 6 },
+		{ "invalid UTF-8/base64", 0xfd, 0xff, 0xfe, "base64",
+		    ARTIFACT_PREVIEW_HEAD_ENCODED_MAX / 4 * 3,
+		    ARTIFACT_PREVIEW_TAIL_ENCODED_MAX / 4 * 3 }
 	};
 	struct artifact_result in;
-	struct json j;
 	struct buf out;
-	char *encoding;
 	u_char *raw;
-	size_t elen, hi, i, lo, mid;
-	int root;
+	size_t i, max;
 
 	raw = malloc(ARTIFACT_RESULT_MAX + 1);
 	CHECK(raw != NULL);
@@ -251,49 +418,38 @@ test_exact_inline_boundaries(void)
 		return;
 	buf_init(&out);
 	for (i = 0; i < sizeof(shapes) / sizeof(shapes[0]); i++) {
-		memset(raw, shapes[i].byte, ARTIFACT_RESULT_MAX + 1);
-		lo = 0;
-		hi = ARTIFACT_RESULT_MAX + 1;
-		while (lo + 1 < hi) {
-			mid = lo + (hi - lo) / 2;
-			if (artifact_result_inline_fits(raw, mid))
-				lo = mid;
-			else
-				hi = mid;
-		}
-		CHECK(lo != 0 && artifact_result_inline_fits(raw, lo));
-		CHECK(!artifact_result_inline_fits(raw, lo + 1));
+		max = shapes[i].head_raw_max + shapes[i].tail_raw_max;
+		memset(raw, shapes[i].head_byte, shapes[i].head_raw_max);
+		memset(raw + shapes[i].head_raw_max, shapes[i].tail_byte,
+		    shapes[i].tail_raw_max);
+		CHECK(artifact_result_inline_fits(raw, max));
 
 		memset(&in, 0, sizeof(in));
 		in.raw = raw;
-		in.raw_len = lo;
-		in.observed_bytes = lo;
-		in.retained_bytes = lo;
+		in.raw_len = max;
+		in.observed_bytes = max;
+		in.retained_bytes = max;
 		in.termination = ARTIFACT_TERM_EXIT;
 		CHECK(artifact_result_build(&out, &in) == 0 &&
-		    out.len <= ARTIFACT_RESULT_MAX &&
-		    ARTIFACT_RESULT_MAX - out.len < shapes[i].encoded_step);
-		CHECK(json_parse(&j, out.data, out.len, 0) == 0);
-		root = json_root(&j);
-		CHECK(json_get_bool(&j, json_obj_get(&j, root,
-		    "preview_abbreviated")) == 0);
-		encoding = string_field(&j, root, "preview_encoding", &elen);
-		CHECK(encoding != NULL && elen == strlen(shapes[i].encoding) &&
-		    memcmp(encoding, shapes[i].encoding, elen) == 0);
-		free(encoding);
-		json_done(&j);
+		    out.len <= ARTIFACT_RESULT_MAX);
+		check_preview_result(&out, raw, max, shapes[i].head_raw_max,
+		    shapes[i].tail_raw_max, shapes[i].encoding, 0);
+		streamed_matches_contiguous(raw, max, 7919);
 
-		in.raw_len = lo + 1;
-		in.observed_bytes = lo + 1;
-		in.retained_bytes = lo + 1;
+		memmove(raw + shapes[i].head_raw_max + 1,
+		    raw + shapes[i].head_raw_max, shapes[i].tail_raw_max);
+		raw[shapes[i].head_raw_max] = shapes[i].marker_byte;
+		CHECK(!artifact_result_inline_fits(raw, max + 1));
+		in.raw_len = max + 1;
+		in.observed_bytes = max + 1;
+		in.retained_bytes = max + 1;
 		in.id = good_id;
 		CHECK(artifact_result_build(&out, &in) == 0 &&
 		    out.len <= ARTIFACT_RESULT_MAX);
-		CHECK(json_parse(&j, out.data, out.len, 0) == 0);
-		root = json_root(&j);
-		CHECK(json_get_bool(&j, json_obj_get(&j, root,
-		    "preview_abbreviated")) == 1);
-		json_done(&j);
+		check_preview_result(&out, raw, max + 1,
+		    shapes[i].head_raw_max, shapes[i].tail_raw_max,
+		    shapes[i].encoding, 1);
+		streamed_matches_contiguous(raw, max + 1, 16381);
 		(void)shapes[i].name;
 	}
 	buf_free(&out);
@@ -301,6 +457,75 @@ test_exact_inline_boundaries(void)
 }
 
 static void
+test_multibyte_inline_boundary(void)
+{
+	static const u_char euro[] = { 0xe2, 0x82, 0xac };
+	static const u_char heart[] = { 0xe2, 0x99, 0xa5 };
+	static const u_char snowman[] = { 0xe2, 0x98, 0x83 };
+	struct artifact_result in;
+	struct buf out;
+	u_char *raw;
+	size_t head_len, i, max, tail_len;
+
+	head_len = ARTIFACT_PREVIEW_HEAD_ENCODED_MAX / sizeof(euro) *
+	    sizeof(euro);
+	tail_len = ARTIFACT_PREVIEW_TAIL_ENCODED_MAX / sizeof(snowman) *
+	    sizeof(snowman);
+	max = head_len + tail_len;
+	raw = malloc(max + sizeof(heart));
+	CHECK(raw != NULL);
+	if (raw == NULL)
+		return;
+	for (i = 0; i < head_len; i += sizeof(euro))
+		memcpy(raw + i, euro, sizeof(euro));
+	for (i = 0; i < tail_len; i += sizeof(snowman))
+		memcpy(raw + head_len + i, snowman, sizeof(snowman));
+	CHECK(artifact_result_inline_fits(raw, max));
+
+	memset(&in, 0, sizeof(in));
+	in.raw = raw;
+	in.raw_len = max;
+	in.observed_bytes = max;
+	in.retained_bytes = max;
+	in.termination = ARTIFACT_TERM_EXIT;
+	buf_init(&out);
+	CHECK(artifact_result_build(&out, &in) == 0 &&
+	    out.len <= ARTIFACT_RESULT_MAX);
+	check_preview_result(&out, raw, max, head_len, tail_len, "utf-8", 0);
+	streamed_matches_contiguous(raw, max, 4093);
+
+	/* A complete rune between the exact spans is omitted without splitting
+	 * either multibyte boundary. */
+	memmove(raw + head_len + sizeof(heart), raw + head_len, tail_len);
+	memcpy(raw + head_len, heart, sizeof(heart));
+	CHECK(!artifact_result_inline_fits(raw, max + sizeof(heart)));
+	in.raw_len = max + sizeof(heart);
+	in.observed_bytes = in.raw_len;
+	in.retained_bytes = in.raw_len;
+	in.id = good_id;
+	CHECK(artifact_result_build(&out, &in) == 0);
+	check_preview_result(&out, raw, in.raw_len, head_len, tail_len,
+	    "utf-8", 1);
+	streamed_matches_contiguous(raw, in.raw_len, 8191);
+
+	/* A partial rune makes the complete stream binary; base64 still selects
+	 * the exact earliest and latest raw bytes within its own budgets. */
+	raw[head_len] = heart[0];
+	memmove(raw + head_len + 1, raw + head_len + sizeof(heart), tail_len);
+	in.raw_len = max + 1;
+	in.observed_bytes = in.raw_len;
+	in.retained_bytes = in.raw_len;
+	CHECK(!artifact_result_inline_fits(raw, in.raw_len));
+	CHECK(artifact_result_build(&out, &in) == 0);
+	check_preview_result(&out, raw, in.raw_len,
+	    ARTIFACT_PREVIEW_HEAD_ENCODED_MAX / 4 * 3,
+	    ARTIFACT_PREVIEW_TAIL_ENCODED_MAX / 4 * 3, "base64", 1);
+	streamed_matches_contiguous(raw, in.raw_len, 12289);
+	buf_free(&out);
+	free(raw);
+}
+
+static void
 test_error_metadata_is_not_spoofable(void)
 {
 	static const char detail[] = "spawn failed before capture";
@@ -427,6 +652,7 @@ test_escape_heavy_error_budget(void)
 {
 	struct artifact_result in;
 	struct buf raw, error, out;
+	size_t head_encoded, tail_encoded;
 
 	buf_init(&raw);
 	buf_init(&error);
@@ -447,6 +673,13 @@ test_escape_heavy_error_budget(void)
 	in.error_len = error.len;
 	CHECK(artifact_result_build(&out, &in) == 0);
 	CHECK(out.len <= ARTIFACT_RESULT_MAX);
+	head_encoded = encoded_field_len(&out, "preview_head");
+	tail_encoded = encoded_field_len(&out, "preview_tail");
+	CHECK(head_encoded <= ARTIFACT_PREVIEW_HEAD_ENCODED_MAX &&
+	    tail_encoded <= ARTIFACT_PREVIEW_TAIL_ENCODED_MAX &&
+	    out.len >= head_encoded + tail_encoded &&
+	    out.len - head_encoded - tail_encoded <=
+	    ARTIFACT_RESULT_META_RESERVE);
 	buf_addc(&error, '\1');
 	in.error_len = error.len;
 	CHECK(artifact_result_build(&out, &in) == -1);
@@ -540,6 +773,7 @@ main(void)
 	test_binary_base64();
 	test_boundary_selection();
 	test_exact_inline_boundaries();
+	test_multibyte_inline_boundary();
 	test_error_metadata_is_not_spoofable();
 	test_json_integer_range();
 	test_empty_streaming_capture_is_inline();
blob - de0a9757de6e43f56feae09c70cb629d4c2e5c52
blob + 010b0e3a50f631a8baadbb405958703a312b986d
--- src/fugu/artifact_result.c
+++ src/fugu/artifact_result.c
@@ -153,6 +153,39 @@ base64_raw_max(size_t budget)
 }
 
 static void
+preview_spans(const u_char *raw, size_t len, int utf8,
+    struct preview_span *head, struct preview_span *tail)
+{
+	size_t maxraw;
+
+	if (utf8) {
+		utf8_head(raw, len, ARTIFACT_PREVIEW_HEAD_ENCODED_MAX, head);
+		utf8_tail(raw, len, ARTIFACT_PREVIEW_TAIL_ENCODED_MAX, tail);
+		if (head->len + tail->len > len) {
+			tail->data = raw + head->len;
+			tail->len = len - head->len;
+		}
+		return;
+	}
+	maxraw = base64_raw_max(ARTIFACT_PREVIEW_HEAD_ENCODED_MAX);
+	head->data = raw;
+	head->len = len < maxraw ? len : maxraw;
+	maxraw = base64_raw_max(ARTIFACT_PREVIEW_TAIL_ENCODED_MAX);
+	tail->len = len - head->len;
+	if (tail->len > maxraw)
+		tail->len = maxraw;
+	tail->data = raw + len - tail->len;
+}
+
+static int
+preview_complete(const u_char *raw, size_t len,
+    const struct preview_span *head, const struct preview_span *tail)
+{
+	return (head->data == raw && tail->data == raw + head->len &&
+	    head->len <= len && tail->len == len - head->len);
+}
+
+static void
 base64_add(struct buf *out, const u_char *p, size_t len)
 {
 	static const char table[] =
@@ -336,36 +369,15 @@ emit_result(struct buf *out, const struct artifact_res
 	const u_char *raw = in->raw != NULL ? in->raw : &empty;
 	struct preview_span head, tail;
 	int utf8, abbreviated;
-	size_t maxraw;
 
 	if (result_valid(in, 1) == -1)
 		return (-1);
 	utf8 = valid_utf8(raw, in->raw_len);
+	preview_spans(raw, in->raw_len, utf8, &head, &tail);
 	abbreviated = !inline_ok;
-	if (!abbreviated) {
-		head.data = raw;
-		head.len = in->raw_len;
-		tail.data = raw + in->raw_len;
-		tail.len = 0;
-	} else if (utf8) {
-		utf8_head(raw, in->raw_len,
-		    ARTIFACT_PREVIEW_HEAD_ENCODED_MAX, &head);
-		utf8_tail(raw, in->raw_len,
-		    ARTIFACT_PREVIEW_TAIL_ENCODED_MAX, &tail);
-		if (head.len + tail.len > in->raw_len) {
-			tail.data = raw + head.len;
-			tail.len = in->raw_len - head.len;
-		}
-	} else {
-		maxraw = base64_raw_max(ARTIFACT_PREVIEW_HEAD_ENCODED_MAX);
-		head.data = raw;
-		head.len = in->raw_len < maxraw ? in->raw_len : maxraw;
-		maxraw = base64_raw_max(ARTIFACT_PREVIEW_TAIL_ENCODED_MAX);
-		tail.len = in->raw_len - head.len;
-		if (tail.len > maxraw)
-			tail.len = maxraw;
-		tail.data = raw + in->raw_len - tail.len;
-	}
+	if (!abbreviated &&
+	    !preview_complete(raw, in->raw_len, &head, &tail))
+		return (-1);
 
 	return (emit_spans(out, in, &head, &tail, utf8, abbreviated));
 }