summaryrefslogblamecommitdiffstats
path: root/_build/content/articles/erlang-validate-utf8.asciidoc
blob: dd0c1e106890f5de7f990ebff57caa406bf5d814 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15














                                                                                     

                                                                      
























































































































































































                                                                                                    
+++
date = "2015-03-06T00:00:00+01:00"
title = "Validating UTF-8 binaries with Erlang"

+++

Yesterday I pushed Websocket permessage-deflate to
Cowboy master. I also pushed
https://github.com/ninenines/cowlib/commit/7e4983b70ddf8cedb967e36fba6a600731bdad5d[a
change in the way the code validates UTF-8 data]
(required for text and close frames as per the spec).

When looking into why the permessage-deflate tests
in autobahntestsuite were taking such a long time, I
found that autobahn is using an adaptation of the
algorithm named http://bjoern.hoehrmann.de/utf-8/decoder/dfa/[Flexible
and Economical UTF-8 Decoder]. This is the C99
implementation:

[source,c]
----
// Copyright (c) 2008-2009 Bjoern Hoehrmann <[email protected]>
// See http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ for details.

#define UTF8_ACCEPT 0
#define UTF8_REJECT 1

static const uint8_t utf8d[] = {
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 00..1f
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 20..3f
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 40..5f
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 60..7f
  1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, // 80..9f
  7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, // a0..bf
  8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, // c0..df
  0xa,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x4,0x3,0x3, // e0..ef
  0xb,0x6,0x6,0x6,0x5,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8, // f0..ff
  0x0,0x1,0x2,0x3,0x5,0x8,0x7,0x1,0x1,0x1,0x4,0x6,0x1,0x1,0x1,0x1, // s0..s0
  1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,1, // s1..s2
  1,2,1,1,1,1,1,2,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1, // s3..s4
  1,2,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,3,1,1,1,1,1,1, // s5..s6
  1,3,1,1,1,1,1,3,1,3,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // s7..s8
};

uint32_t inline
decode(uint32_t* state, uint32_t* codep, uint32_t byte) {
  uint32_t type = utf8d[byte];

  *codep = (*state != UTF8_ACCEPT) ?
    (byte & 0x3fu) | (*codep << 6) :
    (0xff >> type) & (byte);

  *state = utf8d[256 + *state*16 + type];
  return *state;
}
----

And this is the Erlang implementation I came up with:

[source,erlang]
----
%% This function returns 0 on success, 1 on error, and 2..8 on incomplete data.
validate_utf8(<<>>, State) -> State;
validate_utf8(<< C, Rest/bits >>, 0) when C < 128 -> validate_utf8(Rest, 0);
validate_utf8(<< C, Rest/bits >>, 2) when C >= 128, C < 144 -> validate_utf8(Rest, 0);
validate_utf8(<< C, Rest/bits >>, 3) when C >= 128, C < 144 -> validate_utf8(Rest, 2);
validate_utf8(<< C, Rest/bits >>, 5) when C >= 128, C < 144 -> validate_utf8(Rest, 2);
validate_utf8(<< C, Rest/bits >>, 7) when C >= 128, C < 144 -> validate_utf8(Rest, 3);
validate_utf8(<< C, Rest/bits >>, 8) when C >= 128, C < 144 -> validate_utf8(Rest, 3);
validate_utf8(<< C, Rest/bits >>, 2) when C >= 144, C < 160 -> validate_utf8(Rest, 0);
validate_utf8(<< C, Rest/bits >>, 3) when C >= 144, C < 160 -> validate_utf8(Rest, 2);
validate_utf8(<< C, Rest/bits >>, 5) when C >= 144, C < 160 -> validate_utf8(Rest, 2);
validate_utf8(<< C, Rest/bits >>, 6) when C >= 144, C < 160 -> validate_utf8(Rest, 3);
validate_utf8(<< C, Rest/bits >>, 7) when C >= 144, C < 160 -> validate_utf8(Rest, 3);
validate_utf8(<< C, Rest/bits >>, 2) when C >= 160, C < 192 -> validate_utf8(Rest, 0);
validate_utf8(<< C, Rest/bits >>, 3) when C >= 160, C < 192 -> validate_utf8(Rest, 2);
validate_utf8(<< C, Rest/bits >>, 4) when C >= 160, C < 192 -> validate_utf8(Rest, 2);
validate_utf8(<< C, Rest/bits >>, 6) when C >= 160, C < 192 -> validate_utf8(Rest, 3);
validate_utf8(<< C, Rest/bits >>, 7) when C >= 160, C < 192 -> validate_utf8(Rest, 3);
validate_utf8(<< C, Rest/bits >>, 0) when C >= 194, C < 224 -> validate_utf8(Rest, 2);
validate_utf8(<< 224, Rest/bits >>, 0) -> validate_utf8(Rest, 4);
validate_utf8(<< C, Rest/bits >>, 0) when C >= 225, C < 237 -> validate_utf8(Rest, 3);
validate_utf8(<< 237, Rest/bits >>, 0) -> validate_utf8(Rest, 5);
validate_utf8(<< C, Rest/bits >>, 0) when C =:= 238; C =:= 239 -> validate_utf8(Rest, 3);
validate_utf8(<< 240, Rest/bits >>, 0) -> validate_utf8(Rest, 6);
validate_utf8(<< C, Rest/bits >>, 0) when C =:= 241; C =:= 242; C =:= 243 -> validate_utf8(Rest, 7);
validate_utf8(<< 244, Rest/bits >>, 0) -> validate_utf8(Rest, 8);
validate_utf8(_, _) -> 1.
----

Does it look similar to you? So how did we get there?

I started with a naive implementation of the original. First, we
don't need the codepoint calculated and extracted for our validation
function. We just want to know the data is valid, so we only need to
calculate the next state. Then, the only thing we needed to be careful
about was that tuples are 1-based, and that we need to stop processing
the binary when we get the state 1 or when the binary is empty.

[source,erlang]
----
validate_utf8(<<>>, State) -> State;
validate_utf8(_, 1) -> 1;
validate_utf8(<< C, Rest/bits >>, State) ->
	validate_utf8(Rest, element(257 + State * 16 + element(1 + C, ?UTF8D), ?UTF8D)).
----

The macro `?UTF8D` is the tuple equivalent of the C array
in the original code.

Compared to our previous algorithm, this performed about the same.
In some situations a little faster, in some a little slower. In other words,
not good enough. But because this new algorithm allows us to avoid a binary
concatenation this warranted looking further.

It was time to step into crazy land.

Erlang is very good at pattern matching, even more so than doing some
arithmetic coupled by fetching elements from a tuple. So I decided I was
going to write all possible clauses for all combinations of `C`
and `State`. And by write I mean generate.

So I opened my Erlang shell, defined the variable `D` to be
the tuple `?UTF8D` with its 400 elements, and then ran the
following expression (after a bit of trial and error):

[source,erlang]
----
16> file:write_file("out.txt",
	[io_lib:format("validate_utf8(<< ~p, Rest/bits >>, ~p) -> ~p;~n",
		[C, S, element(257 + S * 16 + element(1 + C, D), D)])
			|| C <- lists:seq(0,255), S <- lists:seq(0,8)]).
ok
----

The result is a 2304 lines long file, containing 2304 clauses.
People who pay attention to what I say on Twitter will remember
I said something around 3000 clauses, but that was just me not
using the right number of states in my estimate.

There was a little more work to be done on this generated
code that I did using regular expressions. We need to recurse
when the resulting state is not 1. We also need to stop when
the binary is empty, making it the 2305th clause.

Still, 2305 is a lot. But hey, the code did work, and faster
than the previous implementation too! But hey, perhaps I could
find a way to reduce its size.

Removing all the clauses that return 1 and putting a catch-all
clause at the end instead reduced the number to about 500, and
showed that many clauses were similar:

[source,erlang]
----
validate_utf8(<< 0, Rest/bits >>, 0) -> validate_utf8(Rest, 0);
validate_utf8(<< 1, Rest/bits >>, 0) -> validate_utf8(Rest, 0);
validate_utf8(<< 2, Rest/bits >>, 0) -> validate_utf8(Rest, 0);
validate_utf8(<< 3, Rest/bits >>, 0) -> validate_utf8(Rest, 0);
validate_utf8(<< 4, Rest/bits >>, 0) -> validate_utf8(Rest, 0);
validate_utf8(<< 5, Rest/bits >>, 0) -> validate_utf8(Rest, 0);
validate_utf8(<< 6, Rest/bits >>, 0) -> validate_utf8(Rest, 0);
validate_utf8(<< 7, Rest/bits >>, 0) -> validate_utf8(Rest, 0);
----

But also:

[source,erlang]
----
validate_utf8(<< 157, Rest/bits >>, 2) -> validate_utf8(Rest, 0);
validate_utf8(<< 157, Rest/bits >>, 3) -> validate_utf8(Rest, 2);
validate_utf8(<< 157, Rest/bits >>, 5) -> validate_utf8(Rest, 2);
validate_utf8(<< 157, Rest/bits >>, 6) -> validate_utf8(Rest, 3);
validate_utf8(<< 157, Rest/bits >>, 7) -> validate_utf8(Rest, 3);
validate_utf8(<< 158, Rest/bits >>, 2) -> validate_utf8(Rest, 0);
validate_utf8(<< 158, Rest/bits >>, 3) -> validate_utf8(Rest, 2);
validate_utf8(<< 158, Rest/bits >>, 5) -> validate_utf8(Rest, 2);
validate_utf8(<< 158, Rest/bits >>, 6) -> validate_utf8(Rest, 3);
validate_utf8(<< 158, Rest/bits >>, 7) -> validate_utf8(Rest, 3);
----

Patterns, my favorites!

A little more time was spent to edit the 500 or so clauses into
smaller equivalents, testing that performance was not impacted, and
comitting the result.

The patterns above can be found here in the resulting function:

[source,erlang]
----
validate_utf8(<< C, Rest/bits >>, 0) when C < 128 -> validate_utf8(Rest, 0);
...
validate_utf8(<< C, Rest/bits >>, 2) when C >= 144, C < 160 -> validate_utf8(Rest, 0);
validate_utf8(<< C, Rest/bits >>, 3) when C >= 144, C < 160 -> validate_utf8(Rest, 2);
validate_utf8(<< C, Rest/bits >>, 5) when C >= 144, C < 160 -> validate_utf8(Rest, 2);
validate_utf8(<< C, Rest/bits >>, 6) when C >= 144, C < 160 -> validate_utf8(Rest, 3);
validate_utf8(<< C, Rest/bits >>, 7) when C >= 144, C < 160 -> validate_utf8(Rest, 3);
...
----

I hope you enjoyed this post.