From b5d4cb91f80c833795a2d87050c3674bb7aecdc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Tue, 3 Oct 2017 13:39:41 +0200 Subject: Update Hugo, docs --- articles/erlang-validate-utf8/index.html | 474 +++++++++++++++++-------------- 1 file changed, 263 insertions(+), 211 deletions(-) (limited to 'articles/erlang-validate-utf8') diff --git a/articles/erlang-validate-utf8/index.html b/articles/erlang-validate-utf8/index.html index a96aba26..3eceb840 100644 --- a/articles/erlang-validate-utf8/index.html +++ b/articles/erlang-validate-utf8/index.html @@ -7,7 +7,7 @@ - + Nine Nines: Validating UTF-8 binaries with Erlang @@ -74,191 +74,191 @@

-

Yesterday I pushed Websocket permessage-deflate to -Cowboy master. I also pushed -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 Flexible -and Economical UTF-8 Decoder. This is the C99 -implementation:

-
-
-
// Copyright (c) 2008-2009 Bjoern Hoehrmann <bjoern@hoehrmann.de>
-// 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:

-
-
-
%% 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.

-
-
-
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):

-
-
-
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:

-
-
-
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:

-
-
-
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:

-
-
-
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.

+

Yesterday I pushed Websocket permessage-deflate to +Cowboy master. I also pushed +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 Flexible +and Economical UTF-8 Decoder. This is the C99 +implementation:

+
+
+
// Copyright (c) 2008-2009 Bjoern Hoehrmann <bjoern@hoehrmann.de>
+// 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:

+
+
+
%% 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.

+
+
+
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):

+
+
+
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:

+
+
+
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:

+
+
+
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:

+
+
+
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.

@@ -267,55 +267,107 @@ http://www.gnu.org/software/src-highlite -->

More articles

-- cgit v1.2.3