View Issue Details

IDProjectCategoryView StatusLast Update
0002979Far ManagerEditorpublic2015-04-05 08:31
ReporterSEt Assigned To 
PrioritynormalSeverityfeatureReproducibilityN/A
Status newResolutionopen 
Product Version3.0 
Summary0002979: Редактирование любых файлов без потерь в режиме UTF-8
DescriptionСейчас редактор Far'а крайне убого работает с файлами в многобайтовых кодировках (в частности, UTF-8), содержащими непреобразуемые символы: при чтении они заменяются на один и тот же стандартный символ-заменитель, при записи он же и пишется, напрочь убивая что там было двоичное вокруг текста.

На самом деле, никаких серьёзных причин вести себя так нет и 'неконвертируемые' символы вполне можно сохранять. Например, для UTF-8<->UTF-16 преобразования есть модификация, использующая непарные вторые части суррогатных пар для сохранения 'неконвертируемых' символов, что позволяет в лучшем случае (если все преобразования - модифицированные) выполнить без потерь конвертацию UTF-8->UTF-16->UTF-8 для любых данных, а в худшем (часть преобразований - обычные) - получить стандартное поведение с символом-заменителем.

Прилагается демо-патч к Far'у, с минимальными модификациями заменяющий его конверсии на модифицированные, что позволяет без потерь редактировать что угодно в UTF-8.
TagsNo tags attached.
Build

Activities

SEt

2015-04-02 01:09

reporter  

lossless_utf-8.diff (4,971 bytes)   
Index: unicode_far/codepage.cpp
===================================================================
--- unicode_far/codepage.cpp	(revision 13032)
+++ unicode_far/codepage.cpp	(working copy)
@@ -1205,6 +1205,12 @@
 
 //################################################################################################
 
+#if 1
+#define INVALID_CHAR 0xDC00 + c1
+#else
+#define INVALID_CHAR -1
+#endif
+
 int Utf8::ToWideChar(const char *s, int nc, wchar_t *w1,wchar_t *w2, int wlen, int &tail )
 {
 	bool need_one = wlen <= 0;
@@ -1220,7 +1226,7 @@
 		if (c1 < 0x80) // simple ASCII
 			wc = (wchar_t)c1;
 		else if ( c1 < 0xC2 || c1 >= 0xF5 ) // illegal 1-st byte
-			wc = -1;
+			wc = INVALID_CHAR;
 		else
 		{ // multibyte (2, 3, 4)
 			if (ic + 0 >= nc )
@@ -1245,7 +1251,7 @@
 				|| (0xF4 == c1 && c2 >= 0x90) // illegal 4-byte (out of unicode range)
 				)
 			{
-				wc = -1;
+				wc = INVALID_CHAR;
 			}
 			else if ( c1 < 0xE0 )
 			{ // legal 2-byte
@@ -1258,11 +1264,16 @@
 					goto unfinished;
 				unsigned char c3 = ((const unsigned char *)s)[ic+1];
 				if ( 0x80 != (c3 & 0xC0) ) // illegal 3-rd byte
-					wc = -1;
+					wc = INVALID_CHAR;
 				else if ( c1 < 0xF0 )
 				{ // legal 3-byte
+					wc = ((c1 & 0x0F) << 12) | ((c2 & 0x3F) << 6) | (c3 & 0x3F);
+#if 1
+					if (wc - 0xD800 <= 0xDFFF - 0xD800) // invalid: surrogate area code
+						wc = INVALID_CHAR;
+					else
+#endif
 					ic += 2;
-					wc = ((c1 & 0x0F) << 12) | ((c2 & 0x3F) << 6) | (c3 & 0x3F);
 				}
 				else
 				{ // 4-byte
@@ -1271,7 +1282,7 @@
 
 					unsigned char c4 = ((const unsigned char *)s)[ic+2];
 					if ( 0x80 != (c4 & 0xC0) ) // illegal 4-th byte
-						wc = -1;
+						wc = INVALID_CHAR;
 					else
 					{ // legal 4-byte (produce 2 WCHARs)
 						ic += 3;
Index: unicode_far/fileedit.cpp
===================================================================
--- unicode_far/fileedit.cpp	(revision 13032)
+++ unicode_far/fileedit.cpp	(working copy)
@@ -1750,6 +1750,70 @@
 	return loaded != FALSE;
 }
 
+size_t UTF16toUTF8B(const wchar_t *src, size_t len, char *dst)
+{
+	const wchar_t *end = src + len;
+	if (!dst) // buf counting mode
+	{
+		size_t result = 0;
+		while (src < end)
+		{
+			unsigned int c = *src++;
+			if (c < 0x80)
+				result += 1;
+			else if (c < 0x800)
+				result += 2;
+			else if (c - 0xD800 > 0xDFFF - 0xD800) // not surrogates
+				result += 3;
+			else if (c - 0xDC80 <= 0xDCFF - 0xDC80) // embedded raw byte
+				result += 1;
+			else if (c - 0xD800 <= 0xDBFF - 0xD800 && src < end && *src - 0xDC00u <= 0xDFFF - 0xDC00) // valid surrogate pair
+				result += 4, src++;
+			else
+				result += 3; // invalid
+		}
+		return result;
+	}
+	// conversion mode
+	while (src < end)
+	{
+		unsigned int c = *src++;
+		if (c < 0x80)
+			*dst++ = c;
+		else if (c < 0x800)
+		{
+			dst[0] = 0xC0 + (c >> 6);
+			dst[1] = 0x80 + (c & 0x3F);
+			dst += 2;
+		}
+		else if (c - 0xD800 > 0xDFFF - 0xD800) // not surrogates
+		{
+		l:
+			dst[0] = 0xE0 + (c >> 12);
+			dst[1] = 0x80 + (c >> 6 & 0x3F);
+			dst[2] = 0x80 + (c & 0x3F);
+			dst += 3;
+		}
+		else if (c - 0xDC80 <= 0xDCFF - 0xDC80) // embedded raw byte
+			*dst++ = c & 0xFF;
+		else if (c - 0xD800 <= 0xDBFF - 0xD800 && src < end && *src - 0xDC00u <= 0xDFFF - 0xDC00) // valid surrogate pair
+		{
+			c = 0x3C10000 + ((c - 0xD800) << 10) + (unsigned int)*src++ - 0xDC00;
+			dst[0] = c >> 18;
+			dst[1] = 0x80 + (c >> 12 & 0x3F);
+			dst[2] = 0x80 + (c >> 6 & 0x3F);
+			dst[3] = 0x80 + (c & 0x3F);
+			dst += 4;
+		}
+		else
+		{
+			c = 0xFFFD; // invalid: mapped to 'Replacement Character'
+			goto l;
+		}
+	}
+	return 0;
+}
+
 //TextFormat � codepage ��������� ������, ���� bSaveAs = true!
 int FileEditor::SaveFile(const string& Name,int Ask, bool bSaveAs, int TextFormat, uintptr_t codepage, bool AddSignature)
 {
@@ -2095,7 +2159,11 @@
 
 				if (Length)
 				{
-					size_t length = codepage == CP_REVERSEBOM? Length * sizeof(wchar_t) : WideCharToMultiByte(codepage, 0, SaveStr, static_cast<int>(Length), nullptr, 0, nullptr, nullptr);
+					size_t length = codepage == CP_REVERSEBOM? Length * sizeof(wchar_t) :
+#if 1
+						codepage == CP_UTF8 ? UTF16toUTF8B(SaveStr, Length, nullptr) :
+#endif
+						WideCharToMultiByte(codepage, 0, SaveStr, static_cast<int>(Length), nullptr, 0, nullptr, nullptr);
 					char_ptr SaveStrCopy(length);
 
 					if (SaveStrCopy)
@@ -2103,6 +2171,11 @@
 						if (codepage == CP_REVERSEBOM)
 							SwapBytes(SaveStr, SaveStrCopy.get(), length);
 						else
+#if 1
+							if (codepage == CP_UTF8)
+								UTF16toUTF8B(SaveStr, Length, SaveStrCopy.get());
+							else
+#endif
 							WideCharToMultiByte(codepage, 0, SaveStr, static_cast<int>(Length), SaveStrCopy.get(), static_cast<int>(length), nullptr, nullptr);
 
 						if (!Cache.Write(SaveStrCopy.get(),length))
lossless_utf-8.diff (4,971 bytes)   

zg

2015-04-02 14:44

developer   bugnote:0013607

> что позволяет без потерь редактировать что угодно в UTF-8
открываю файл с содержимым:
EF BB BF F4 FB E2 E0
сохраняю:
EF BB BF F4 FB E2 EF BF BD
безпотерьности не наблюдаю.

SEt

2015-04-02 17:54

reporter  

lossless_utf-8_v2.diff (5,246 bytes)   
Index: unicode_far/codepage.cpp
===================================================================
--- unicode_far/codepage.cpp	(revision 13032)
+++ unicode_far/codepage.cpp	(working copy)
@@ -1205,6 +1205,12 @@
 
 //################################################################################################
 
+#if 1
+#define INVALID_CHAR 0xDC00 + c1
+#else
+#define INVALID_CHAR -1
+#endif
+
 int Utf8::ToWideChar(const char *s, int nc, wchar_t *w1,wchar_t *w2, int wlen, int &tail )
 {
 	bool need_one = wlen <= 0;
@@ -1220,7 +1226,7 @@
 		if (c1 < 0x80) // simple ASCII
 			wc = (wchar_t)c1;
 		else if ( c1 < 0xC2 || c1 >= 0xF5 ) // illegal 1-st byte
-			wc = -1;
+			wc = INVALID_CHAR;
 		else
 		{ // multibyte (2, 3, 4)
 			if (ic + 0 >= nc )
@@ -1231,9 +1237,16 @@
 				else
 				{
 					tail = 0;
+#if 1
+					wc = 0xDC00 + c1;
+					w1[0] = wc;
+					if (w2)
+						w2[0] = wc;
+#else
 					w1[0] = Utf::REPLACE_CHAR;
 					if (w2)
 						w2[0] = L'?';
+#endif
 					nw = 1;
 				}
 				return nw;
@@ -1245,7 +1258,7 @@
 				|| (0xF4 == c1 && c2 >= 0x90) // illegal 4-byte (out of unicode range)
 				)
 			{
-				wc = -1;
+				wc = INVALID_CHAR;
 			}
 			else if ( c1 < 0xE0 )
 			{ // legal 2-byte
@@ -1258,11 +1271,16 @@
 					goto unfinished;
 				unsigned char c3 = ((const unsigned char *)s)[ic+1];
 				if ( 0x80 != (c3 & 0xC0) ) // illegal 3-rd byte
-					wc = -1;
+					wc = INVALID_CHAR;
 				else if ( c1 < 0xF0 )
 				{ // legal 3-byte
+					wc = ((c1 & 0x0F) << 12) | ((c2 & 0x3F) << 6) | (c3 & 0x3F);
+#if 1
+					if (wc - 0xD800 <= 0xDFFF - 0xD800) // invalid: surrogate area code
+						wc = INVALID_CHAR;
+					else
+#endif
 					ic += 2;
-					wc = ((c1 & 0x0F) << 12) | ((c2 & 0x3F) << 6) | (c3 & 0x3F);
 				}
 				else
 				{ // 4-byte
@@ -1271,7 +1289,7 @@
 
 					unsigned char c4 = ((const unsigned char *)s)[ic+2];
 					if ( 0x80 != (c4 & 0xC0) ) // illegal 4-th byte
-						wc = -1;
+						wc = INVALID_CHAR;
 					else
 					{ // legal 4-byte (produce 2 WCHARs)
 						ic += 3;
Index: unicode_far/fileedit.cpp
===================================================================
--- unicode_far/fileedit.cpp	(revision 13032)
+++ unicode_far/fileedit.cpp	(working copy)
@@ -1750,6 +1750,70 @@
 	return loaded != FALSE;
 }
 
+size_t UTF16toUTF8B(const wchar_t *src, size_t len, char *dst)
+{
+	const wchar_t *end = src + len;
+	if (!dst) // buf counting mode
+	{
+		size_t result = 0;
+		while (src < end)
+		{
+			unsigned int c = *src++;
+			if (c < 0x80)
+				result += 1;
+			else if (c < 0x800)
+				result += 2;
+			else if (c - 0xD800 > 0xDFFF - 0xD800) // not surrogates
+				result += 3;
+			else if (c - 0xDC80 <= 0xDCFF - 0xDC80) // embedded raw byte
+				result += 1;
+			else if (c - 0xD800 <= 0xDBFF - 0xD800 && src < end && *src - 0xDC00u <= 0xDFFF - 0xDC00) // valid surrogate pair
+				result += 4, src++;
+			else
+				result += 3; // invalid
+		}
+		return result;
+	}
+	// conversion mode
+	while (src < end)
+	{
+		unsigned int c = *src++;
+		if (c < 0x80)
+			*dst++ = c;
+		else if (c < 0x800)
+		{
+			dst[0] = 0xC0 + (c >> 6);
+			dst[1] = 0x80 + (c & 0x3F);
+			dst += 2;
+		}
+		else if (c - 0xD800 > 0xDFFF - 0xD800) // not surrogates
+		{
+		l:
+			dst[0] = 0xE0 + (c >> 12);
+			dst[1] = 0x80 + (c >> 6 & 0x3F);
+			dst[2] = 0x80 + (c & 0x3F);
+			dst += 3;
+		}
+		else if (c - 0xDC80 <= 0xDCFF - 0xDC80) // embedded raw byte
+			*dst++ = c & 0xFF;
+		else if (c - 0xD800 <= 0xDBFF - 0xD800 && src < end && *src - 0xDC00u <= 0xDFFF - 0xDC00) // valid surrogate pair
+		{
+			c = 0x3C10000 + ((c - 0xD800) << 10) + (unsigned int)*src++ - 0xDC00;
+			dst[0] = c >> 18;
+			dst[1] = 0x80 + (c >> 12 & 0x3F);
+			dst[2] = 0x80 + (c >> 6 & 0x3F);
+			dst[3] = 0x80 + (c & 0x3F);
+			dst += 4;
+		}
+		else
+		{
+			c = 0xFFFD; // invalid: mapped to 'Replacement Character'
+			goto l;
+		}
+	}
+	return 0;
+}
+
 //TextFormat � codepage ��������� ������, ���� bSaveAs = true!
 int FileEditor::SaveFile(const string& Name,int Ask, bool bSaveAs, int TextFormat, uintptr_t codepage, bool AddSignature)
 {
@@ -2095,7 +2159,11 @@
 
 				if (Length)
 				{
-					size_t length = codepage == CP_REVERSEBOM? Length * sizeof(wchar_t) : WideCharToMultiByte(codepage, 0, SaveStr, static_cast<int>(Length), nullptr, 0, nullptr, nullptr);
+					size_t length = codepage == CP_REVERSEBOM? Length * sizeof(wchar_t) :
+#if 1
+						codepage == CP_UTF8 ? UTF16toUTF8B(SaveStr, Length, nullptr) :
+#endif
+						WideCharToMultiByte(codepage, 0, SaveStr, static_cast<int>(Length), nullptr, 0, nullptr, nullptr);
 					char_ptr SaveStrCopy(length);
 
 					if (SaveStrCopy)
@@ -2103,6 +2171,11 @@
 						if (codepage == CP_REVERSEBOM)
 							SwapBytes(SaveStr, SaveStrCopy.get(), length);
 						else
+#if 1
+							if (codepage == CP_UTF8)
+								UTF16toUTF8B(SaveStr, Length, SaveStrCopy.get());
+							else
+#endif
 							WideCharToMultiByte(codepage, 0, SaveStr, static_cast<int>(Length), SaveStrCopy.get(), static_cast<int>(length), nullptr, nullptr);
 
 						if (!Cache.Write(SaveStrCopy.get(),length))
lossless_utf-8_v2.diff (5,246 bytes)   

SEt

2015-04-02 18:00

reporter   bugnote:0013608

Да, было такое, не всё исправил в текущем коде Far'а - в v2 версии патча концовка должна обрабатываться правильно.

Ещё безпотерьность может запороть условие после
//??? caller should be fixed to avoid this...
но я не знаю, как его адекватно вправить.

2useven10

2015-04-02 22:06

developer   bugnote:0013609

> но я не знаю, как его адекватно вправить.
Этот код выполняться не должен.
Если такое всё ещё происходит, то адекватной будет правка вызывателя.

zg

2015-04-03 01:00

developer   bugnote:0013610

4332.

2useven10

2015-04-03 04:46

developer   bugnote:0013611

Скорее всего будут проблемы в нетекстовых режимах просмотра.
Там нужно 'честное' преобразование.

SEt

2015-04-03 18:22

reporter   bugnote:0013612

Это вполне 'честное' преобразование, но, конечно, желательно везде использовать один и тот же вариант преобразования.

Сейчас шестнадцатеричный режим просмотра съезжает, так как там используется
    int clen = WideCharToMultiByte(CP_UTF8, 0, w2.data()+iw, 1, nullptr,0, nullptr,nullptr);
При замене на
    int clen = Utf8::ToMultiByte(w2.data()+iw, 1, nullptr);
всё нормализуется.

Странному коду чуть выше по угадыванию продолжения символа в начале строки это не помогает, и его хорошо бы как-то более адекватно реализовать.

2useven10

2015-04-05 07:36

developer   bugnote:0013614

Last edited: 2015-04-05 08:31

Теперь любое преобразование в UTF-8 успешное.
Ошибок не бывает. А иногда именно их и надо найти.
Можно, конечно, сделать поиск по диапазону, но это дополнительное неочевидное телодвижение.

Часть кода в старом варианте преобразования нужна только для нахождения ошибок. Вариант с двумя выходными буферами нужен (был) для того, чтобы при обратном преобразовании получить ту же длину строки, что и в оригинале.
В новом варианте, если мы на нём останавливаемся, это всё не нужно. Можно упростить, но нужна ревизия кода, где используется преобразование.

2useven10

2015-04-05 07:47

developer   bugnote:0013615

Ещё надо бы проверить смену кодировки.
Скорее всего там нужны отдельные приседания для utf-8.

Issue History

Date Modified Username Field Change
2015-04-02 01:09 SEt New Issue
2015-04-02 01:09 SEt File Added: lossless_utf-8.diff
2015-04-02 14:44 zg Note Added: 0013607
2015-04-02 17:54 SEt File Added: lossless_utf-8_v2.diff
2015-04-02 18:00 SEt Note Added: 0013608
2015-04-02 22:06 2useven10 Note Added: 0013609
2015-04-03 01:00 zg Note Added: 0013610
2015-04-03 04:46 2useven10 Note Added: 0013611
2015-04-03 18:22 SEt Note Added: 0013612
2015-04-05 07:36 2useven10 Note Added: 0013614
2015-04-05 07:47 2useven10 Note Added: 0013615
2015-04-05 08:31 2useven10 Note Edited: 0013614