Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/administration/accounts.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -245,10 +245,11 @@ void Account::set_password(const std::string &password) {
}

bool Account::compare_password(const std::string &password) {
// Тот же приём, что в Password::compare_password: хэш посчитан по дисковым байтам,
// поэтому пароль приводим к дисковой кодировке перед crypt (issue #3681).
// Тот же приём, что в Password::compare_password: хэши посчитаны по кои-восьмым байтам,
// поэтому пароль приводим к той же форме перед crypt. Это формат сохранённых хэшей,
// а не кодировка диска.
return CompareParam(this->hash_password,
CRYPT(native_text::to_disk(password).c_str(), this->hash_password.c_str()), true);
CRYPT(native_text::to_koi8(password).c_str(), this->hash_password.c_str()), true);
}

bool Account::quest_is_available(int id) {
Expand Down
16 changes: 8 additions & 8 deletions src/administration/password.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@

namespace Password {

// Хэш пароля -- сохранённые данные, и посчитан он когда-то по дисковым байтам (KOI8-R).
// Движок теперь держит текст нативным, поэтому кириллический пароль дал бы другие байты
// и не сошёлся бы с сохранённым хэшем. Приводим к дисковой форме перед crypt: старые хэши
// продолжают сходиться, а новые остаются пригодными для отката (issue #3681).
// Хэш пароля -- сохранённые данные, и посчитан он когда-то по кои-восьмым байтам. Движок
// держит текст в UTF-8, поэтому кириллический пароль дал бы другие байты и не сошёлся бы
// с сохранённым хэшем. Перекодировка здесь -- не граница диска (её больше нет), а формат
// самих сохранённых хэшей: сменить его можно только перехэшированием при успешном входе.
static std::string password_bytes(const std::string &pwd) {
return native_text::to_disk(pwd);
return native_text::to_koi8(pwd);
}

const char *BAD_PASSWORD = "Пароль должен быть от 8 до 50 символов и не должен быть именем персонажа.";
Expand All @@ -44,9 +44,9 @@ const unsigned int MAX_PWD_LENGTH = 50;
// * Генерация хэша с более-менее рандомным сальтом
std::string generate_md5_hash(const std::string &pwd) {
#ifdef NOCRYPT
// И здесь дисковая форма: сравнение всё равно приводит пароль к ней, а хранить
// нативную значило бы, что кириллический пароль не сойдётся сам с собой. Сборки
// без crypt() -- это Windows, macOS и -Dnocrypt=true (issue #3681).
// И здесь та же форма: сравнение всё равно приводит пароль к ней, а хранить
// UTF-8 значило бы, что кириллический пароль не сойдётся сам с собой. Сборки
// без crypt() -- это Windows, macOS и -Dnocrypt=true.
return password_bytes(pwd);
#else
char key[14];
Expand Down
6 changes: 3 additions & 3 deletions src/engine/db/obj_save.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1595,9 +1595,9 @@ int Crash_load(CharData *ch) {
// Считается по дисковым байтам -- до перевода в нативную кодировку, иначе сумма не сойдётся.
FileCRC::verify_from_content(ch->get_uid(), FileCRC::kTextObjs, readdata, fsize);

// Граница чтения: файл лежит в кодировке мира (сейчас KOI8-R), в память вещи идут
// нативными -- зеркало к to_disk на записи. Без этого имена, алиасы и метки вещей
// уезжают в транслит при первом же сохранении (issue #3681).
// Граница чтения: файл на диске в UTF-8, но написанный до миграции ещё может оказаться
// кои-восьмым. from_disk_text распознаёт то и другое; без него имена, алиасы и метки
// вещей уезжали в транслит при первом же сохранении (issue #3681).
{
const std::string native = native_text::from_disk_text(std::string(readdata, static_cast<std::size_t>(fsize)));
free(readdata);
Expand Down
6 changes: 3 additions & 3 deletions src/engine/network/descriptor_data.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,9 @@ void DescriptorData::string_to_client_encoding(const char *in_str, char *out_str
// поэтому перед ними текст надо привести к KOI8-R. Под KOI8-R-рантаймом это тождество,
// под UTF-8 - настоящая перекодировка (issue #3681). Для UTF-8-клиента ничего приводить
// не нужно: см. case kCodePageUTF8 ниже.
// Зеркало предохранителя из to_disk. Всё нативное -- валидный UTF-8; если сюда пришло
// иное, значит текст прочитан с диска мимо границы и игрок увидит кашу. Так уже уезжали
// экран справки и список синонимов (issue #3681).
// Предохранитель. Всё нативное -- валидный UTF-8; если сюда пришло иное, значит текст
// прочитан с диска мимо границы и игрок увидит кашу. Так уже уезжали экран справки
// и список синонимов (issue #3681).
//
// Проверка -- полный разбор строки, быстрого выхода на латинице в is_valid нет. Замерено:
// 436 нс на строку в 208 байт, около 477 МБ/с. Для легаси-клиентов это заметно дешевле
Expand Down
6 changes: 3 additions & 3 deletions src/gameplay/clans/house.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2539,9 +2539,9 @@ void Clan::ChestLoad() {
}
fclose(fl);

// Граница чтения: сундук лежит на диске в кодировке мира, в память идёт нативным --
// зеркало к to_disk в ChestSaver. Без этого имена и метки вещей в сундуке уезжают
// в транслит при первом же сохранении дружины (issue #3681).
// Граница чтения: сундук на диске в UTF-8, но написанный до миграции файл ещё может
// оказаться кои-восьмым. from_disk_text распознаёт то и другое; без него имена и метки
// вещей в сундуке уезжали в транслит при первом же сохранении дружины (issue #3681).
{
const std::string native = native_text::from_disk_text(std::string(databuf, static_cast<std::size_t>(fsize)));
delete[] databuf;
Expand Down
2 changes: 1 addition & 1 deletion src/gameplay/communication/mail.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -595,7 +595,7 @@ void save() {
// (read_data_file) принимает и старый KOI8-R (issue #3787).
std::ostringstream xml;
doc.save(xml, "\t", pugi::format_default, pugi::encoding_utf8);
native_text::write_file_native(MAIL_XML_FILE, xml.str());
native_text::write_file(MAIL_XML_FILE, xml.str());
need_save = false;
}

Expand Down
4 changes: 1 addition & 3 deletions src/gameplay/crafting/craft.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1548,10 +1548,8 @@ bool CCraftModel::export_object(const ObjVnum vnum, const char *filename) {

pugi::xml_node decl = document.prepend_child(pugi::node_declaration);
decl.append_attribute("version") = "1.0";
decl.append_attribute("encoding") = "koi8-r";
decl.append_attribute("encoding") = "utf-8";

// Граница записи: XML уходит на диск в кодировке мира, а не в нативной. Объявление выше
// так и заявляет koi8-r, значит и байты должны быть koi8-r (issue #3681).
std::ostringstream xml;
document.save(xml, "\t", pugi::format_default, pugi::encoding_utf8);
return native_text::write_file(filename, xml.str());
Expand Down
6 changes: 3 additions & 3 deletions src/gameplay/economics/exchange.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -896,9 +896,9 @@ int LoadExchange() {
};
fclose(fl);

// Граница чтения: база лежит на диске в KOI8-R, а движок работает с текстом в нативной
// кодировке. Без этого названия лотов оставались бы байтами чужой кодировки -- и на экране,
// и при обратной записи через to_disk, которая приняла бы их за UTF-8 (issue #3681).
// Граница чтения: база на диске в UTF-8, но файл, написанный до миграции, ещё может
// оказаться кои-восьмым. from_disk_text распознаёт то и другое; без него названия лотов
// остались бы байтами чужой кодировки прямо на экране (issue #3681).
const std::string native = native_text::from_disk_text(std::string(raw.data(), actual_size));
CREATE(readdata, native.size() + 1);
memcpy(readdata, native.c_str(), native.size() + 1);
Expand Down
3 changes: 1 addition & 2 deletions src/gameplay/mechanics/glory_const.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -857,7 +857,6 @@ void do_glory(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) {

void save() {
pugi::xml_document doc;
doc.append_attribute("encoding") = "koi8-r";
doc.append_child().set_name("glory_list");
pugi::xml_node char_list = doc.child("glory_list");

Expand Down Expand Up @@ -896,7 +895,7 @@ void save() {
// (read_data_file) принимает и старый KOI8-R (issue #3787).
std::ostringstream xml;
doc.save(xml, "\t", pugi::format_default, pugi::encoding_utf8);
native_text::write_file_native(LIB_USERDATA"glory_const.xml", xml.str());
native_text::write_file(LIB_USERDATA"glory_const.xml", xml.str());
}

void load() {
Expand Down
2 changes: 1 addition & 1 deletion src/gameplay/mechanics/named_stuff.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ void save() {
// (read_data_file) принимает и старый KOI8-R (issue #3787).
std::ostringstream xml;
doc.save(xml, "\t", pugi::format_default, pugi::encoding_utf8);
native_text::write_file_native(LIB_USERDATA"named_items.xml", xml.str());
native_text::write_file(LIB_USERDATA"named_items.xml", xml.str());
}

bool check_named(CharData *ch, const ObjData *obj, const bool simple) {
Expand Down
69 changes: 24 additions & 45 deletions src/utils/native_text.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -373,10 +373,6 @@ std::string to_koi8(const std::string &text) {
return codepages::Utf8ToKoi8(text);
}

std::string from_utf8(const std::string &text) {
return text; // the native encoding already is UTF-8
}

std::string translit_to_filename(std::string_view name) {
// Code point -> the very same Latin character the KOI8-R byte table yields, so a player's
// file name is identical before and after the flip. Upper and lower case collapse together
Expand Down Expand Up @@ -544,17 +540,33 @@ std::size_t char_offset(std::string_view s, std::size_t chars) {
return utf8::byte_offset(s, chars);
}


bool write_file(const std::string &path, const std::string &text) {
const std::string on_disk = to_disk(text);
std::ofstream out(path, std::ios::binary);
if (!out) {
return false;
// Предохранитель, переехавший сюда из to_disk. Всё нативное -- валидный UTF-8; если
// сюда пришло иное, значит строка не проходила границу чтения и держит дисковые байты
// (KOI8-R) как есть. Пишем их всё равно -- файл остаётся цел, -- но жалуемся в лог:
// граница чтения ещё нужна, файл до миграции всё ещё может попасться, а без этой
// жалобы пропущенная граница уезжает на диск молча.
if (!utf8::is_valid(text)) {
static std::atomic<unsigned long> seen{0};
const unsigned long n = seen.fetch_add(1);
if (n < 10 || n % 10000 == 0) {
// Байты печатаются шестнадцатеричными нарочно: невалидный UTF-8 в сообщении
// логгер погнал бы обратно через эту же запись.
std::string head;
const std::size_t show = std::min<std::size_t>(text.size(), 16);
char byte[4];
for (std::size_t i = 0; i < show; ++i) {
std::snprintf(byte, sizeof(byte), "%02x", static_cast<unsigned char>(text[i]));
head += byte;
head += ' ';
}
log("SYSERR: write_file got non-UTF-8 text (#%lu, %zu bytes, %s) -- a read boundary "
"is missing somewhere; writing the bytes through unchanged. First bytes: %s",
n + 1, text.size(), path.c_str(), head.c_str());
}
}
out.write(on_disk.data(), static_cast<std::streamsize>(on_disk.size()));
return out.good();
}

bool write_file_native(const std::string &path, const std::string &text) {
std::ofstream out(path, std::ios::binary);
if (!out) {
return false;
Expand All @@ -572,39 +584,6 @@ std::string pad_right(std::string_view s, std::size_t width) {
return out;
}

std::string to_disk(const std::string &text) {
// Предохранитель. Всё нативное -- валидный UTF-8; если сюда пришло что-то другое, значит
// строка не проходила границу чтения и держит дисковые байты (KOI8-R) как есть.
// Транслитерировать их нельзя: to_koi8 разберёт такие байты как Latin-1 и прогонит через
// словарь замен, а это необратимо -- 'верий.свет' превращается в 'AIEUAxAOA.OxAO'. Именно
// так были съедены метки вещей, сундуки дружин и списки имён (issue #3681).
//
// Поэтому пишем байты как есть -- для диска они уже в нужной кодировке, файл остаётся цел, --
// и жалуемся в лог: дыру видно сразу, без нагрузочного прогона и без потери данных.
if (!utf8::is_valid(text)) {
static std::atomic<unsigned long> seen{0};
const unsigned long n = seen.fetch_add(1);
if (n < 10 || n % 10000 == 0) {
// Байты печатаются шестнадцатеричными нарочно: сунуть их в сообщение как есть
// значило бы отдать логгеру невалидный UTF-8, а он пишет через этот же to_disk --
// и жалоба принялась бы жаловаться сама на себя без конца.
std::string head;
const std::size_t show = std::min<std::size_t>(text.size(), 16);
char byte[4];
for (std::size_t i = 0; i < show; ++i) {
std::snprintf(byte, sizeof(byte), "%02x", static_cast<unsigned char>(text[i]));
head += byte;
head += ' ';
}
log("SYSERR: to_disk got non-UTF-8 text (#%lu, %zu bytes) -- a read boundary is missing "
"somewhere; writing the bytes through unchanged. First bytes: %s",
n + 1, text.size(), head.c_str());
}
return text;
}
return to_koi8(text);
}

std::string read_data_file(const std::string &path) {
std::ifstream in(path, std::ios::binary);
if (!in) {
Expand Down
32 changes: 6 additions & 26 deletions src/utils/native_text.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ LOWER(*s) or s[0] = UPPER(s[0]) is wrong on a multibyte letter and was the singl
of bugs in the migration.

The conversions at the bottom of this header are boundaries, not helpers for everyday code:
from_disk_* / to_disk for the world files (still KOI8-R on disk), to_koi8 for legacy client code
pages (their tables are indexed by KOI8-R bytes).
to_koi8 / from_koi8 for legacy client code pages (their tables are indexed by KOI8-R bytes), and
from_disk_* for files written before the UTF-8 migration.
*/

#ifndef BYLINS_SRC_UTILS_NATIVE_TEXT_H_
Expand Down Expand Up @@ -204,11 +204,6 @@ std::string from_koi8(const std::string &text);
// equivalent at all becomes the converter's placeholder.
std::string to_koi8(const std::string &text);

// Bring text that is UTF-8 on disk into the native encoding. The counterpart of from_koi8 for
// the files that are deliberately kept in UTF-8 rather than KOI8-R (the login screen). Identity
// under UTF-8; under KOI8-R it goes through the same reduction as to_koi8, so a file written
// with the full Unicode repertoire still renders sensibly on a KOI8-R build.
std::string from_utf8(const std::string &text);

// Collation key for sorting Russian text. The Russian letters are not in alphabetical order in
// KOI8-R, so sorting has always gone through Windows-1251 bytes, where they are. The key
Expand Down Expand Up @@ -238,26 +233,11 @@ std::string from_disk_line(const char *line);
// exactly what happened to cfg/mechanics/obj_sets.xml, issue #3681).
std::string from_disk_text(const std::string &text);

// The write side of the same boundary, and the exact mirror of from_disk_text: whatever the
// engine puts on disk goes out in the encoding the disk format is in, which during the migration
// is still KOI8-R. Identity under KOI8-R; under UTF-8 the text is reduced and transcoded exactly
// as it is for a legacy client (see to_koi8).
//
// Read and write MUST stay symmetric. If the engine writes the native encoding while the rest of
// the world is KOI8-R, then the first save quietly converts every file it touches, rolling back
// to a KOI8-R build stops being possible, and the world is no longer the world we started with.
// (issue #3681).
std::string to_disk(const std::string &text);

// Записать текст в файл в кодировке мира. Однострочная обёртка над to_disk для тех, кто иначе
// звал бы pugi::save_file или свой ofstream и уносил бы на диск нативную кодировку. Возвращает
// false, если файл не открылся (issue #3681).
bool write_file(const std::string &path, const std::string &text);

// То же, но без перевода: для деревьев, которые уже хранятся в нативной кодировке (userdata,
// state, мир). Отдельная функция, а не флаг у write_file, чтобы в месте вызова было видно, в
// какой кодировке лежит файл, и чтобы граница записи читалась рядом с чтением (issue #3787).
bool write_file_native(const std::string &path, const std::string &text);
// Записать текст в файл как есть. Диск и движок в одной кодировке, переводить нечего;
// функция существует ради тех, кто иначе звал бы pugi::save_file или свой ofstream.
// Возвращает false, если файл не открылся.
bool write_file(const std::string &path, const std::string &text);

// Pad `s` on the right with spaces to `width` CHARACTERS. The replacement for printf's "%-Ns"
// wherever the value can hold Russian: printf counts the field width in bytes, so under UTF-8 a
Expand Down
Loading
Loading