Index: uspace/app/bithenge/helenos/os.h
===================================================================
--- uspace/app/bithenge/helenos/os.h	(revision 978ccaf1353e6dc7d83c324cc4bfc3d5a951be8c)
+++ uspace/app/bithenge/helenos/os.h	(revision 600f5d197151eb0ed71c0ce7e030405f7c2231e1)
@@ -35,4 +35,5 @@
 #include <macros.h>
 #include <mem.h>
+#include <stdlib.h>
 #include <str.h>
 #include <str_error.h>
@@ -67,3 +68,11 @@
 }
 
+static inline void *memchr(const void *s, int c, size_t n)
+{
+	for (size_t i = 0; i < n; i++)
+		if (((char *)s)[i] == c)
+			return (void *)(s + i);
+	return NULL;
+}
+
 #endif
Index: uspace/app/bithenge/script.c
===================================================================
--- uspace/app/bithenge/script.c	(revision 978ccaf1353e6dc7d83c324cc4bfc3d5a951be8c)
+++ uspace/app/bithenge/script.c	(revision 600f5d197151eb0ed71c0ce7e030405f7c2231e1)
@@ -177,5 +177,6 @@
 		return;
 	} else if (isalpha(ch)) {
-		while (isalnum(state->buffer[state->buffer_pos]))
+		while (isalnum(state->buffer[state->buffer_pos])
+		    || state->buffer[state->buffer_pos] == '_')
 			state->buffer_pos++;
 		char *value = str_ndup(state->buffer + state->old_buffer_pos,
@@ -308,7 +309,12 @@
 	expect(state, '{');
 	while (state->error == EOK && state->token != '}') {
-		expect(state, '.');
-		subxforms[num].name = expect_identifier(state);
-		expect(state, TOKEN_LEFT_ARROW);
+		if (state->token == '.') {
+			expect(state, '.');
+			subxforms[num].name = expect_identifier(state);
+			expect(state, TOKEN_LEFT_ARROW);
+		} else {
+			subxforms[num].name = NULL;
+			expect(state, TOKEN_LEFT_ARROW);
+		}
 		subxforms[num].transform = parse_transform(state);
 		expect(state, ';');
@@ -339,7 +345,7 @@
 }
 
-/** Parse a transform. 
+/** Parse a transform without composition.
  * @return The parsed transform, or NULL if an error occurred. */
-static bithenge_transform_t *parse_transform(state_t *state)
+static bithenge_transform_t *parse_transform_no_compose(state_t *state)
 {
 	if (state->token == TOKEN_IDENTIFIER) {
@@ -356,4 +362,38 @@
 		return NULL;
 	}
+}
+
+/** Parse a transform.
+ * @return The parsed transform, or NULL if an error occurred. */
+static bithenge_transform_t *parse_transform(state_t *state)
+{
+	bithenge_transform_t *result = parse_transform_no_compose(state);
+	bithenge_transform_t **xforms = NULL;
+	size_t num = 1;
+	while (state->token == TOKEN_LEFT_ARROW) {
+		expect(state, TOKEN_LEFT_ARROW);
+		xforms = state_realloc(state, xforms,
+		    (num + 1) * sizeof(*xforms));
+		if (state->error != EOK)
+			break;
+		xforms[num] = parse_transform_no_compose(state);
+		num++;
+	}
+	if (state->error != EOK) {
+		while (xforms && num--)
+			bithenge_transform_dec_ref(xforms[num]);
+		free(xforms);
+		bithenge_transform_dec_ref(result);
+		return NULL;
+	}
+	if (xforms) {
+		xforms[0] = result;
+		int rc = bithenge_new_composed_transform(&result, xforms, num);
+		if (rc != EOK) {
+			error_errno(state, rc);
+			return NULL;
+		}
+	}
+	return result;
 }
 
Index: uspace/app/bithenge/transform.c
===================================================================
--- uspace/app/bithenge/transform.c	(revision 978ccaf1353e6dc7d83c324cc4bfc3d5a951be8c)
+++ uspace/app/bithenge/transform.c	(revision 600f5d197151eb0ed71c0ce7e030405f7c2231e1)
@@ -61,4 +61,45 @@
 }
 
+static int ascii_apply(bithenge_transform_t *self,
+    bithenge_node_t *in, bithenge_node_t **out)
+{
+	int rc;
+	if (bithenge_node_type(in) != BITHENGE_NODE_BLOB)
+		return EINVAL;
+	bithenge_blob_t *blob = bithenge_node_as_blob(in);
+	aoff64_t size;
+	rc = bithenge_blob_size(blob, &size);
+	if (rc != EOK)
+		return rc;
+
+	char *buffer = malloc(size + 1);
+	if (!buffer)
+		return ENOMEM;
+	aoff64_t size_read = size;
+	rc = bithenge_blob_read(blob, 0, buffer, &size_read);
+	if (rc != EOK) {
+		free(buffer);
+		return rc;
+	}
+	if (size_read != size) {
+		free(buffer);
+		return EINVAL;
+	}
+	buffer[size] = '\0';
+
+	/* TODO: what if the OS encoding is incompatible with ASCII? */
+	return bithenge_new_string_node(out, buffer, true);
+}
+
+static const bithenge_transform_ops_t ascii_ops = {
+	.apply = ascii_apply,
+	.destroy = transform_indestructible,
+};
+
+/** The ASCII text transform. */
+bithenge_transform_t bithenge_ascii_transform = {
+	&ascii_ops, 1
+};
+
 static int uint32le_apply(bithenge_transform_t *self, bithenge_node_t *in,
     bithenge_node_t **out)
@@ -130,7 +171,64 @@
 };
 
+static int zero_terminated_apply(bithenge_transform_t *self,
+    bithenge_node_t *in, bithenge_node_t **out)
+{
+	int rc;
+	if (bithenge_node_type(in) != BITHENGE_NODE_BLOB)
+		return EINVAL;
+	bithenge_blob_t *blob = bithenge_node_as_blob(in);
+	aoff64_t size;
+	rc = bithenge_blob_size(blob, &size);
+	if (rc != EOK)
+		return rc;
+	if (size < 1)
+		return EINVAL;
+	char ch;
+	aoff64_t size_read = 1;
+	rc = bithenge_blob_read(blob, size - 1, &ch, &size_read);
+	if (rc != EOK)
+		return rc;
+	if (size_read != 1 || ch != '\0')
+		return EINVAL;
+	bithenge_blob_inc_ref(blob);
+	return bithenge_new_subblob(out, blob, 0, size - 1);
+}
+
+static int zero_terminated_prefix_length(bithenge_transform_t *self,
+    bithenge_blob_t *blob, aoff64_t *out)
+{
+	int rc;
+	char buffer[4096];
+	aoff64_t offset = 0, size_read = sizeof(buffer);
+	do {
+		rc = bithenge_blob_read(blob, offset, buffer, &size_read);
+		if (rc != EOK)
+			return rc;
+		char *found = memchr(buffer, '\0', size_read);
+		if (found) {
+			*out = found - buffer + offset + 1;
+			return EOK;
+		}
+		offset += size_read;
+	} while (size_read == sizeof(buffer));
+	return EINVAL;
+}
+
+static const bithenge_transform_ops_t zero_terminated_ops = {
+	.apply = zero_terminated_apply,
+	.prefix_length = zero_terminated_prefix_length,
+	.destroy = transform_indestructible,
+};
+
+/** The zero-terminated data transform. */
+bithenge_transform_t bithenge_zero_terminated_transform = {
+	&zero_terminated_ops, 1
+};
+
 static bithenge_named_transform_t primitive_transforms[] = {
+	{"ascii", &bithenge_ascii_transform},
 	{"uint32le", &bithenge_uint32le_transform},
 	{"uint32be", &bithenge_uint32be_transform},
+	{"zero_terminated", &bithenge_zero_terminated_transform},
 	{NULL, NULL}
 };
@@ -367,6 +465,5 @@
 {
 	int rc;
-	struct_transform_t *self =
-	    malloc(sizeof(*self));
+	struct_transform_t *self = malloc(sizeof(*self));
 	if (!self) {
 		rc = ENOMEM;
@@ -386,4 +483,105 @@
 }
 
+typedef struct {
+	bithenge_transform_t base;
+	bithenge_transform_t **xforms;
+	size_t num;
+} compose_transform_t;
+
+static bithenge_transform_t *compose_as_transform(compose_transform_t *xform)
+{
+	return &xform->base;
+}
+
+static compose_transform_t *transform_as_compose(bithenge_transform_t *xform)
+{
+	return (compose_transform_t *)xform;
+}
+
+static int compose_apply(bithenge_transform_t *base, bithenge_node_t *in,
+    bithenge_node_t **out)
+{
+	int rc;
+	compose_transform_t *self = transform_as_compose(base);
+	bithenge_node_inc_ref(in);
+
+	/* i ranges from (self->num - 1) to 0 inside the loop. */
+	for (size_t i = self->num; i--; ) {
+		bithenge_node_t *tmp;
+		rc = bithenge_transform_apply(self->xforms[i], in, &tmp);
+		bithenge_node_dec_ref(in);
+		if (rc != EOK)
+			return rc;
+		in = tmp;
+	}
+
+	*out = in;
+	return rc;
+}
+
+static int compose_prefix_length(bithenge_transform_t *base,
+    bithenge_blob_t *blob, aoff64_t *out)
+{
+	compose_transform_t *self = transform_as_compose(base);
+	return bithenge_transform_prefix_length(self->xforms[self->num - 1],
+	    blob, out);
+}
+
+static void compose_destroy(bithenge_transform_t *base)
+{
+	compose_transform_t *self = transform_as_compose(base);
+	for (size_t i = 0; i < self->num; i++)
+		bithenge_transform_dec_ref(self->xforms[i]);
+	free(self->xforms);
+	free(self);
+}
+
+static const bithenge_transform_ops_t compose_transform_ops = {
+	.apply = compose_apply,
+	.prefix_length = compose_prefix_length,
+	.destroy = compose_destroy,
+};
+
+/** Create a composition of multiple transforms. When the result is applied to a
+ * node, each transform is applied in turn, with the last transform applied
+ * first. @a xforms may contain any number of transforms or no transforms at
+ * all. This function takes ownership of @a xforms and the references therein.
+ * @param[out] out Holds the result.
+ * @param[in] xforms The transforms to apply.
+ * @param num The number of transforms.
+ * @return EOK on success or an error code from errno.h. */
+int bithenge_new_composed_transform(bithenge_transform_t **out,
+    bithenge_transform_t **xforms, size_t num)
+{
+	if (num == 0) {
+		/* TODO: optimize */
+	} else if (num == 1) {
+		*out = xforms[0];
+		free(xforms);
+		return EOK;
+	}
+
+	int rc;
+	compose_transform_t *self = malloc(sizeof(*self));
+	if (!self) {
+		rc = ENOMEM;
+		goto error;
+	}
+	rc = bithenge_init_transform(compose_as_transform(self),
+	    &compose_transform_ops);
+	if (rc != EOK)
+		goto error;
+	self->xforms = xforms;
+	self->num = num;
+	*out = compose_as_transform(self);
+	return EOK;
+error:
+	for (size_t i = 0; i < num; i++)
+		bithenge_transform_dec_ref(xforms[i]);
+	free(xforms);
+	free(self);
+	return rc;
+}
+
 /** @}
  */
Index: uspace/app/bithenge/transform.h
===================================================================
--- uspace/app/bithenge/transform.h	(revision 978ccaf1353e6dc7d83c324cc4bfc3d5a951be8c)
+++ uspace/app/bithenge/transform.h	(revision 600f5d197151eb0ed71c0ce7e030405f7c2231e1)
@@ -118,13 +118,16 @@
 } bithenge_named_transform_t;
 
+extern bithenge_transform_t bithenge_ascii_transform;
 extern bithenge_transform_t bithenge_uint32le_transform;
 extern bithenge_transform_t bithenge_uint32be_transform;
+extern bithenge_transform_t bithenge_zero_terminated_transform;
 extern bithenge_named_transform_t *bithenge_primitive_transforms;
 
 int bithenge_init_transform(bithenge_transform_t *self,
     const bithenge_transform_ops_t *ops);
-
 int bithenge_new_struct(bithenge_transform_t **out,
     bithenge_named_transform_t *subtransforms);
+int bithenge_new_composed_transform(bithenge_transform_t **,
+    bithenge_transform_t **, size_t);
 
 #endif
Index: uspace/app/bithenge/tree.c
===================================================================
--- uspace/app/bithenge/tree.c	(revision 978ccaf1353e6dc7d83c324cc4bfc3d5a951be8c)
+++ uspace/app/bithenge/tree.c	(revision 600f5d197151eb0ed71c0ce7e030405f7c2231e1)
@@ -226,6 +226,9 @@
 	assert(out);
 	bithenge_node_t *self = malloc(sizeof(*self));
-	if (!self)
+	if (!self) {
+		if (needs_free)
+			free((void *)value);
 		return ENOMEM;
+	}
 	self->type = BITHENGE_NODE_STRING;
 	self->refs = 1;
