libpurple/protocols/gg/lib/message.c

changeset 35557
e83a87761544
parent 31683
5fce84b95b78
child 35627
fd11790cc4d6
--- a/libpurple/protocols/gg/lib/message.c	Thu Feb 13 04:27:27 2014 +0100
+++ b/libpurple/protocols/gg/lib/message.c	Thu Feb 13 18:29:10 2014 +0100
@@ -164,7 +164,7 @@
 	GG_MESSAGE_CHECK(gm, (uin_t) -1);
 
 	if ((gm->recipients == NULL) || (gm->recipient_count < 1)) {
-		// errno = XXX;
+		/* errno = XXX; */
 		return (uin_t) -1;
 	}
 
@@ -292,14 +292,14 @@
 
 		free(gm->html_converted);
 
-		len = gg_message_text_to_html(NULL, gm->text, gm->attributes, gm->attributes_length);
+		len = gg_message_text_to_html(NULL, gm->text, GG_ENCODING_UTF8, gm->attributes, gm->attributes_length);
 
 		gm->html_converted = malloc(len + 1);
 
 		if (gm->html_converted == NULL)
 			return NULL;
 
-		gg_message_text_to_html(gm->html_converted, gm->text, gm->attributes, gm->attributes_length);
+		gg_message_text_to_html(gm->html_converted, gm->text, GG_ENCODING_UTF8, gm->attributes, gm->attributes_length);
 
 		return gm->html_converted;
 	}
@@ -312,7 +312,7 @@
 	GG_MESSAGE_CHECK(gm, -1);
 
 	if (length > 0xfffd) {
-		// errno = XXX;
+		/* errno = XXX; */
 		return -1;
 	}
 
@@ -361,7 +361,7 @@
  * \param src Dodawany tekst
  * \param len Długość dodawanego tekstu
  */
-static void gg_append(char *dst, size_t *pos, const void *src, int len)
+static void gg_append(char *dst, size_t *pos, const void *src, size_t len)
 {
 	if (dst != NULL)
 		memcpy(&dst[*pos], src, len);
@@ -373,7 +373,8 @@
  * \internal Zamienia tekst z formatowaniem Gadu-Gadu na HTML.
  *
  * \param dst Bufor wynikowy (może być \c NULL)
- * \param src Tekst źródłowy w UTF-8
+ * \param src Tekst źródłowy
+ * \param encoding Kodowanie tekstu źródłowego oraz wynikowego
  * \param format Atrybuty tekstu źródłowego
  * \param format_len Długość bloku atrybutów tekstu źródłowego
  *
@@ -384,50 +385,50 @@
  *
  * \return Długość tekstu wynikowego bez \c \\0 (nawet jeśli \c dst to \c NULL).
  */
-size_t gg_message_text_to_html(char *dst, const char *src, const char *format, size_t format_len)
+size_t gg_message_text_to_html(char *dst, const char *src, gg_encoding_t encoding, const unsigned char *format, size_t format_len)
 {
 	const char span_fmt[] = "<span style=\"color:#%02x%02x%02x; font-family:'MS Shell Dlg 2'; font-size:9pt; \">";
-	const int span_len = 75;
+	const size_t span_len = 75;
 	const char img_fmt[] = "<img name=\"%02x%02x%02x%02x%02x%02x%02x%02x\">";
-	const int img_len = 29;
-	int char_pos = 0;
-	int format_idx = 0;
+	const size_t img_len = 29;
+	size_t char_pos = 0;
 	unsigned char old_attr = 0;
-	const unsigned char *color = (const unsigned char*) "\x00\x00\x00";
-	int i;
-	size_t len;
-	const unsigned char *format_ = (const unsigned char*) format;
-
-	len = 0;
-
-	/* Nie mamy atrybutów dla pierwsze znaku, a tekst nie jest pusty, więc
-	 * tak czy inaczej trzeba otworzyć <span>. */
-
-	if (src[0] != 0 && (format_idx + 3 > format_len || (format_[format_idx] | (format_[format_idx + 1] << 8)) != 0)) {
-		if (dst != NULL)
-			sprintf(&dst[len], span_fmt, 0, 0, 0);
-
-		len += span_len;
-	}
+	const unsigned char default_color[] = {'\x00', '\x00', '\x00'};
+	const unsigned char *old_color = NULL;
+	int in_span = 0;
+	unsigned int i;
+	size_t len = 0;
 
 	/* Pętla przechodzi też przez kończące \0, żeby móc dokleić obrazek
 	 * na końcu tekstu. */
 
 	for (i = 0; ; i++) {
-		/* Analizuj atrybuty tak długo jak dotyczą aktualnego znaku. */
+		int in_char = 0;
+		size_t format_idx = 0;
+
+		/* Sprawdź, czy bajt jest kontynuacją znaku UTF-8. */
+		if (encoding == GG_ENCODING_UTF8 && (src[i] & 0xc0) == 0x80)
+			in_char = 1;
+
+		/* GG_FONT_IMAGE powinno dotyczyć tylko jednego znaku, więc czyścimy stary atrybut */
+
+		if (!in_char && (old_attr & GG_FONT_IMAGE) != 0)
+			old_attr &= ~GG_FONT_IMAGE;
+
+		/* Analizuj wszystkie atrybuty dotyczące aktualnego znaku. */
 		for (;;) {
 			unsigned char attr;
-			int attr_pos;
+			size_t attr_pos;
+
+			/* Nie wstawiamy niczego wewnątrz wielobajtowego znaku UTF-8. */
+			if (in_char)
+				break;
 
 			if (format_idx + 3 > format_len)
 				break;
 
-			attr_pos = format_[format_idx] | (format_[format_idx + 1] << 8);
-
-			if (attr_pos != char_pos)
-				break;
-
-			attr = format_[format_idx + 2];
+			attr_pos = format[format_idx] | (format[format_idx + 1] << 8);
+			attr = format[format_idx + 2];
 
 			/* Nie doklejaj atrybutów na końcu, co najwyżej obrazki. */
 
@@ -436,37 +437,49 @@
 
 			format_idx += 3;
 
-			if ((attr & (GG_FONT_BOLD | GG_FONT_ITALIC | GG_FONT_UNDERLINE | GG_FONT_COLOR)) != 0 || (attr == 0 && old_attr != 0)) {
-				if (char_pos != 0) {
-					if ((old_attr & GG_FONT_UNDERLINE) != 0)
-						gg_append(dst, &len, "</u>", 4);
+			if (attr_pos != char_pos) {
+				if ((attr & GG_FONT_COLOR) != 0)
+					format_idx += 3;
+				if ((attr & GG_FONT_IMAGE) != 0)
+					format_idx += 10;
+
+				continue;
+			}
 
-					if ((old_attr & GG_FONT_ITALIC) != 0)
-						gg_append(dst, &len, "</i>", 4);
+			if ((old_attr & GG_FONT_UNDERLINE) != 0)
+				gg_append(dst, &len, "</u>", 4);
+
+			if ((old_attr & GG_FONT_ITALIC) != 0)
+				gg_append(dst, &len, "</i>", 4);
 
-					if ((old_attr & GG_FONT_BOLD) != 0)
-						gg_append(dst, &len, "</b>", 4);
+			if ((old_attr & GG_FONT_BOLD) != 0)
+				gg_append(dst, &len, "</b>", 4);
 
-					if (src[i] != 0)
-						gg_append(dst, &len, "</span>", 7);
-				}
+			if ((attr & (GG_FONT_BOLD | GG_FONT_ITALIC | GG_FONT_UNDERLINE | GG_FONT_COLOR)) != 0) {
+				const unsigned char *color;
 
 				if (((attr & GG_FONT_COLOR) != 0) && (format_idx + 3 <= format_len)) {
-					color = &format_[format_idx];
+					color = &format[format_idx];
 					format_idx += 3;
 				} else {
-					color = (unsigned char*) "\x00\x00\x00";
+					color = default_color;
 				}
 
-				if (src[i] != 0) {
-					if (dst != NULL)
-						sprintf(&dst[len], span_fmt, color[0], color[1], color[2]);
-					len += span_len;
+				if (old_color == NULL || memcmp(color, old_color, 3) != 0) {
+					if (in_span) {
+						gg_append(dst, &len, "</span>", 7);
+						in_span = 0;
+					}
+
+					if (src[i] != 0) {
+						if (dst != NULL)
+							sprintf(&dst[len], span_fmt, color[0], color[1], color[2]);
+
+						len += span_len;
+						in_span = 1;
+						old_color = color;
+					}
 				}
-			} else if (char_pos == 0 && src[0] != 0) {
-				if (dst != NULL)
-					sprintf(&dst[len], span_fmt, 0, 0, 0);
-				len += span_len;
 			}
 
 			if ((attr & GG_FONT_BOLD) != 0)
@@ -481,14 +494,14 @@
 			if (((attr & GG_FONT_IMAGE) != 0) && (format_idx + 10 <= format_len)) {
 				if (dst != NULL) {
 					sprintf(&dst[len], img_fmt,
-						format_[format_idx + 9],
-						format_[format_idx + 8], 
-						format_[format_idx + 7],
-						format_[format_idx + 6], 
-						format_[format_idx + 5],
-						format_[format_idx + 4],
-						format_[format_idx + 3],
-						format_[format_idx + 2]);
+						format[format_idx + 9],
+						format[format_idx + 8], 
+						format[format_idx + 7],
+						format[format_idx + 6], 
+						format[format_idx + 5],
+						format[format_idx + 4],
+						format[format_idx + 3],
+						format[format_idx + 2]);
 				}
 
 				len += img_len;
@@ -498,6 +511,30 @@
 			old_attr = attr;
 		}
 
+		if (src[i] == 0)
+			break;
+
+		/* Znaki oznaczone jako GG_FONT_IMAGE nie są częścią wiadomości. */
+
+		if ((old_attr & GG_FONT_IMAGE) != 0) {
+			if (!in_char)
+				char_pos++;
+
+			continue;
+		}
+
+		/* Jesteśmy na początku tekstu i choć nie było atrybutów dla pierwszego
+		* znaku, ponieważ tekst nie jest pusty, trzeba otworzyć <span>. */
+
+		if (!in_span) {
+			if (dst != NULL)
+				sprintf(&dst[len], span_fmt, default_color[0], default_color[1], default_color[2]);
+
+			len += span_len;
+			in_span = 1;
+			old_color = default_color;
+		}
+
 		/* Doklej znak zachowując htmlowe escapowanie. */
 
 		switch (src[i]) {
@@ -520,7 +557,6 @@
 				gg_append(dst, &len, "<br>", 4);
 				break;
 			case '\r':
-			case 0:
 				break;
 			default:
 				if (dst != NULL)
@@ -528,13 +564,8 @@
 				len++;
 		}
 
-		/* Sprawdź, czy bajt nie jest kontynuacją znaku unikodowego. */
-
-		if ((src[i] & 0xc0) != 0xc0)
+		if (!in_char)
 			char_pos++;
-
-		if (src[i] == 0)
-			break;
 	}
 
 	/* Zamknij tagi. */
@@ -548,7 +579,7 @@
 	if ((old_attr & GG_FONT_BOLD) != 0)
 		gg_append(dst, &len, "</b>", 4);
 
-	if (src[0] != 0)
+	if (in_span)
 		gg_append(dst, &len, "</span>", 7);
 
 	if (dst != NULL)
@@ -558,36 +589,104 @@
 }
 
 /**
+ * \internal Dokleja nowe atrybuty formatowania, jeśli konieczne, oraz inkrementuje pozycję znaku w tekście.
+ *
+ * \param pos Wskaźnik na zmienną przechowującą pozycję znaku w tekście
+ * \param attr_flag Aktualna flaga atrybutu formatowania
+ * \param old_attr_flag Wskaźnik na poprzednią flagę atrybutu formatowania
+ * \param color Wskaźnik na tablicę z aktualnym kolorem RGB (jeśli \p attr_flag nie zawiera flagi \c GG_FONT_COLOR, ignorowane)
+ * \param old_color Wskaźnik na tablicę z poprzednim kolorem RGB
+ * \param imgs_size Rozmiar atrybutów formatowania obrazków znajdujących się obecnie w tablicy atrybutów formatowania, w bajtach
+ * \param format Wskaźnik na wskaźnik do tablicy atrybutów formatowania
+ * \param format_len Wskaźnik na zmienną zawierającą długość tablicy atrybutów formatowania, w bajtach (może być \c NULL)
+ */
+static void gg_after_append_formatted_char(uint16_t *pos, unsigned char attr_flag, unsigned char *old_attr_flag, const unsigned char *color, unsigned char *old_color, size_t imgs_size, unsigned char **format, size_t *format_len)
+{
+	const size_t color_size = 3;
+	int has_color = 0;
+
+	if ((attr_flag & GG_FONT_COLOR) != 0)
+		has_color = 1;
+
+	if (*old_attr_flag != attr_flag || (has_color && memcmp(old_color, color, color_size) != 0)) {
+		size_t attr_size = sizeof(*pos) + sizeof(attr_flag) + (has_color ? color_size : 0);
+
+		if (*format != NULL) {
+			/* Staramy się naśladować oryginalnego klienta i atrybuty obrazków trzymamy na końcu */
+
+			*format -= imgs_size;
+			memmove(*format + attr_size, *format, imgs_size);
+
+			**format = (unsigned char) (*pos & (uint16_t) 0x00ffU);
+			*format += 1;
+			**format = (unsigned char) ((*pos & (uint16_t) 0xff00U) >> 8);
+			*format += 1;
+
+			**format = attr_flag;
+			*format += 1;
+
+			if (has_color) {
+				memcpy(*format, color, color_size);
+				*format += color_size;
+			}
+
+			*format += imgs_size;
+		}
+
+		if (format_len != NULL)
+			*format_len += attr_size;
+
+		*old_attr_flag = attr_flag;
+		if (has_color)
+			memcpy(old_color, color, color_size);
+	}
+
+	*pos += 1;
+}
+
+/**
  * \internal Zamienia tekst w formacie HTML na czysty tekst.
  *
  * \param dst Bufor wynikowy (może być \c NULL)
+ * \param format Bufor wynikowy z atrybutami formatowania (może być \c NULL)
+ * \param format_len Wskaźnik na zmienną, do której zostanie zapisana potrzebna wielkość bufora wynikowego z atrybutami formatowania, w bajtach (może być \c NULL)
  * \param html Tekst źródłowy
+ * \param encoding Kodowanie tekstu źródłowego oraz wynikowego
  *
  * \note Dokleja \c \\0 na końcu bufora wynikowego.
  *
- * \note Funkcja służy do zachowania kompatybilności przy przesyłaniu
- * wiadomości HTML do klientów, które tego formatu nie obsługują. Z tego
- * powodu funkcja nie zachowuje formatowania, a jedynie usuwa tagi i
- * zamienia podstawowe encje na ich odpowiedniki ASCII.
- *
- * \return Długość tekstu wynikowego bez \c \\0 (nawet jeśli \c dst to \c NULL).
+ * \return Długość bufora wynikowego bez \c \\0 (nawet jeśli \c dst to \c NULL).
  */
-size_t gg_message_html_to_text(char *dst, const char *html)
+size_t gg_message_html_to_text(char *dst, unsigned char *format, size_t *format_len, const char *html, gg_encoding_t encoding)
 {
-	const char *src, *entity, *tag;
-	int in_tag, in_entity;
-	size_t len;
+	const char *src, *entity = NULL, *tag = NULL;
+	int in_tag = 0, in_entity = 0, in_bold = 0, in_italic = 0, in_underline = 0;
+	unsigned char color[3] = { 0 }, old_color[3] = { 0 };
+	unsigned char attr_flag = 0, old_attr_flag = 0;
+	uint16_t pos = 0;
+	size_t len = 0, imgs_size = 0;
 
-	len = 0;
-	in_tag = 0;
-	tag = NULL;
-	in_entity = 0;
-	entity = NULL;
+	if (format_len != NULL)
+		*format_len = 0;
 
 	for (src = html; *src != 0; src++) {
 		if (in_entity && !(isalnum(*src) || *src == '#' || *src == ';')) {
+			int first = 1;
+			size_t i, append_len = src - entity;
+
+			gg_append(dst, &len, entity, append_len);
+			for (i = 0; i < append_len; i++) {
+				if (encoding != GG_ENCODING_UTF8 || (entity[i] & 0xc0) != 0x80) {
+					if (first) {
+						gg_after_append_formatted_char(&pos, attr_flag, &old_attr_flag, color, old_color, imgs_size, &format, format_len);
+						first = 0;
+					} else {
+						pos++;
+					}
+				}
+			}
+
 			in_entity = 0;
-			gg_append(dst, &len, entity, src - entity);
 		}
 
 		if (*src == '<') {
@@ -601,7 +700,138 @@
 				if (dst != NULL)
 					dst[len] = '\n';
 				len++;
+
+				gg_after_append_formatted_char(&pos, attr_flag, &old_attr_flag, color, old_color, imgs_size, &format, format_len);
+			} else if (strncmp(tag, "<img name=\"", 11) == 0 || strncmp(tag, "<img name=\'", 11) == 0) {
+				tag += 11;
+
+				/* 17 bo jeszcze cudzysłów musi być zamknięty */
+				if (tag + 17 <= src) {
+					int i, ok = 1;
+
+					for (i = 0; i < 16; i++) {
+						if ((tag[i] < '0' || tag[i] > '9') && (tolower(tag[i]) < 'a' || tolower(tag[i]) > 'f')) {
+							ok = 0;
+							break;
+						}
+					}
+
+					if (ok) {
+						unsigned char img_attr[13];
+
+						if (format != NULL) {
+							char buf[3] = { 0 };
+
+							img_attr[0] = (unsigned char) (pos & (uint16_t) 0x00ffU);
+							img_attr[1] = (unsigned char) ((pos & (uint16_t) 0xff00U) >> 8);
+							img_attr[2] = GG_FONT_IMAGE;
+							img_attr[3] = '\x09';
+							img_attr[4] = '\x01';
+							for (i = 0; i < 16; i += 2) {
+								buf[0] = tag[i];
+								buf[1] = tag[i + 1];
+								/* buf[2] to '\0' */
+								img_attr[12 - i / 2] = (unsigned char) strtoul(buf, NULL, 16);
+							}
+
+							memcpy(format, img_attr, sizeof(img_attr));
+							format += sizeof(img_attr);
+						}
+
+						if (format_len != NULL)
+							*format_len += sizeof(img_attr);
+						imgs_size += sizeof(img_attr);
+
+						if (dst != NULL) {
+							if (encoding == GG_ENCODING_UTF8)
+								dst[len++] = '\xc2';
+							dst[len++] = '\xa0';
+						} else {
+							len += 2;
+						}
+
+						/* Nie używamy tutaj gg_after_append_formatted_char(). Po pierwsze to praktycznie niczego
+						 * by nie zmieniło, a po drugie nie wszystkim klientom mogłaby się spodobać redefinicja
+						 * atrybutów formatowania dla jednego znaku (bo np. najpierw byśmy zdefiniowali bolda od
+						 * znaku 10, a potem by się okazało, że znak 10 to obrazek).
+						 */
+
+						pos++;
+
+						/* Resetujemy atrybuty, aby je w razie czego redefiniować od następnego znaku, co by sobie
+						 * nikt przypadkiem nie pomyślał, że GG_FONT_IMAGE dotyczy więcej, niż jednego znaku.
+						 * Tak samo robi oryginalny klient.
+						 */
+
+						old_attr_flag = -1;
+					}
+				}
+			} else if (strncmp(tag, "<b>", 3) == 0) {
+				in_bold++;
+				attr_flag |= GG_FONT_BOLD;
+			} else if (strncmp(tag, "</b>", 4) == 0) {
+				if (in_bold > 0) {
+					in_bold--;
+					if (in_bold == 0)
+						attr_flag &= ~GG_FONT_BOLD;
+				}
+			} else if (strncmp(tag, "<i>", 3) == 0) {
+				in_italic++;
+				attr_flag |= GG_FONT_ITALIC;
+			} else if (strncmp(tag, "</i>", 4) == 0) {
+				if (in_italic > 0) {
+					in_italic--;
+					if (in_italic == 0)
+						attr_flag &= ~GG_FONT_ITALIC;
+				}
+			} else if (strncmp(tag, "<u>", 3) == 0) {
+				in_underline++;
+				attr_flag |= GG_FONT_UNDERLINE;
+			} else if (strncmp(tag, "</u>", 4) == 0) {
+				if (in_underline > 0) {
+					in_underline--;
+					if (in_underline == 0)
+						attr_flag &= ~GG_FONT_UNDERLINE;
+				}
+			} else if (strncmp(tag, "<span ", 6) == 0) {
+				for (tag += 6; tag < src - 8; tag++) {
+					if (*tag == '\"' || *tag == '\'' || *tag == ' ') {
+						if (strncmp(tag + 1, "color:#", 7) == 0) {
+							int i, ok = 1;
+							char buf[3] = { 0 };
+
+							tag += 8;
+							if (tag + 6 > src)
+								break;
+
+							for (i = 0; i < 6; i++) {
+								if ((tag[i] < '0' || tag[i] > '9') && (tolower(tag[i]) < 'a' || tolower(tag[i]) > 'f')) {
+									ok = 0;
+									break;
+								}
+							}
+
+							if (!ok)
+								break;
+
+							for (i = 0; i < 6; i += 2) {
+								buf[0] = tag[i];
+								buf[1] = tag[i + 1];
+								/* buf[2] to '\0' */
+								color[i / 2] = (unsigned char) strtoul(buf, NULL, 16);
+							}
+
+							attr_flag |= GG_FONT_COLOR;
+						}
+					}
+				}
+			} else if (strncmp(tag, "</span", 6) == 0) {
+				/* Można by trzymać kolory na stosie i tutaj przywracać poprzedni, ale to raczej zbędne */
+
+				attr_flag &= ~GG_FONT_COLOR;
 			}
+
+			tag = NULL;
 			in_tag = 0;
 			continue;
 		}
@@ -617,6 +847,7 @@
 
 		if (in_entity && *src == ';') {
 			in_entity = 0;
+
 			if (dst != NULL) {
 				if (strncmp(entity, "&lt;", 4) == 0)
 					dst[len++] = '<';
@@ -629,8 +860,9 @@
 				else if (strncmp(entity, "&amp;", 5) == 0)
 					dst[len++] = '&';
 				else if (strncmp(entity, "&nbsp;", 6) == 0) {
-					dst[len++] = 0xc2;
-					dst[len++] = 0xa0;
+					if (encoding == GG_ENCODING_UTF8)
+						dst[len++] = '\xc2';
+					dst[len++] = '\xa0';
 				} else
 					dst[len++] = '?';
 			} else {
@@ -640,6 +872,8 @@
 					len++;
 			}
 
+			gg_after_append_formatted_char(&pos, attr_flag, &old_attr_flag, color, old_color, imgs_size, &format, format_len);
+
 			continue;
 		}
 
@@ -651,13 +885,94 @@
 
 		if (dst != NULL)
 			dst[len] = *src;
+		len++;
 
-		len++;
+		if (encoding != GG_ENCODING_UTF8 || (*src & 0xc0) != 0x80)
+			gg_after_append_formatted_char(&pos, attr_flag, &old_attr_flag, color, old_color, imgs_size, &format, format_len);
 	}
 
 	if (dst != NULL)
-		dst[len] = 0;
+		dst[len] = '\0';
 	
 	return len;
 }
 
+static size_t gg_message_html_to_text_110_buff(char *dst, const char *html)
+{
+	return gg_message_html_to_text(dst, NULL, NULL, html, GG_ENCODING_UTF8);
+}
+
+static size_t gg_message_text_to_html_110_buff(char *dst, const char *text,
+	ssize_t text_len)
+{
+	size_t i, dst_len;
+
+	if (text_len == -1)
+		text_len = strlen(text);
+	dst_len = 0;
+
+	gg_append(dst, &dst_len, "<span>", 6);
+
+	for (i = 0; i < (size_t)text_len; i++)
+	{
+		char c = text[i];
+		if (c == '<')
+			gg_append(dst, &dst_len, "&lt;", 4);
+		else if (c == '>')
+			gg_append(dst, &dst_len, "&gt;", 4);
+		else if (c == '&')
+			gg_append(dst, &dst_len, "&amp;", 5);
+		else if (c == '"')
+			gg_append(dst, &dst_len, "&quot;", 6);
+		else if (c == '\'')
+			gg_append(dst, &dst_len, "&apos;", 6);
+		else if (c == '\n')
+			gg_append(dst, &dst_len, "<br>", 4);
+		else if (c == '\r')
+			continue;
+		else if (c == '\xc2' && text[i + 1] == '\xa0') {
+			gg_append(dst, &dst_len, "&nbsp;", 6);
+			i++;
+		} else {
+			if (dst)
+				dst[dst_len] = c;
+			dst_len++;
+		}
+	}
+
+	gg_append(dst, &dst_len, "</span>", 7);
+
+	if (dst)
+		dst[dst_len] = '\0';
+
+	return dst_len;
+}
+
+char *gg_message_html_to_text_110(const char *html)
+{
+	size_t dst_len;
+	char *dst;
+
+	dst_len = gg_message_html_to_text_110_buff(NULL, html) + 1;
+	dst = malloc(dst_len);
+	if (!dst)
+		return NULL;
+	gg_message_html_to_text_110_buff(dst, html);
+	
+	return dst;
+}
+
+char *gg_message_text_to_html_110(const char *text, ssize_t text_len)
+{
+	size_t dst_len;
+	char *dst;
+
+	dst_len = gg_message_text_to_html_110_buff(NULL, text, text_len) + 1;
+	dst = malloc(dst_len);
+	if (!dst)
+		return NULL;
+	gg_message_text_to_html_110_buff(dst, text, text_len);
+	
+	return dst;
+}
+

mercurial