diff --git a/README.md b/README.md index 22805821..0424b822 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,17 @@ В апреле 2017 года автор проекта [Валерий Агеев](https://bitbucket.org/awa15/) поделился с сообществом [исходными кодами проекта](https://bitbucket.org/awa15/tool_1cd-sources/src). Изначально проект был написан на Borland C++ Builder, здесь мы занимаемся переносом на "ванильный C++" с поддержкой компиляторов gcc, mingw и msvc и постепенным переходом на более свежие стандарты языка C++. +## Поддерживаемые версии + +Программа работает с файлами `1CD` по версии формата, записанной в заголовке базы: + +| Версия формата | Режим | +|---|---| +| 8.0.3, 8.0.5 | только чтение | +| 8.1.0, 8.2.0, 8.2.14, 8.3.8 | чтение и запись | + +Формат `8.3.8` используют все платформы 1С:Предприятие 8.3.8 и новее (в том числе 8.3.x и 8.5.x) — их базы открываются как `8.3.8`. Версия формата в заголовке не обязательно совпадает с версией платформы, создавшей базу. Базы с другой версией формата не поддерживаются. + ## Установка Проект в процессе разработки. @@ -98,5 +109,17 @@ sudo apt-get install ctool1cd Найти потерянные объекты и сохранить. + `-sts, -SaveTablesSize <путь>` + + Сохранить размеры таблиц базы в указанный файл. + + `-ct, -ClearTable <таблица>` + + Пометить удалёнными все записи указанной таблицы. Команда изменяет базу. + + `-dr, -DeleteRecord <таблица> <номер>` + + Пометить удалённой одну запись таблицы по её физическому номеру. Команда изменяет базу. + Если в пути содержатся пробелы, его необходимо заключать в кавычки. Пути следует указывать без завершающего слеша `/` и бэкслеша `\` Для команд `-dc`, `-ddc`, `-drc` вместо пути можно указывать имя файла конфигурации (имя файла должно заканчиваться на `.cf`). diff --git a/src/ctool1cd/App.cpp b/src/ctool1cd/App.cpp index 2dde0f7d..5e13cdcb 100644 --- a/src/ctool1cd/App.cpp +++ b/src/ctool1cd/App.cpp @@ -30,6 +30,8 @@ #include "ParseCommandLine.h" #include "ErrorCode.h" #include "Messenger.h" +#include "Table.h" +#include "TableRecord.h" extern Registrator msreg_g; @@ -288,6 +290,56 @@ void App::import_from_binary(const ParsedCommand &pc) } // import_from_binary +void App::clear_table(const ParsedCommand &pc) +{ + const string tname = LowerCase(pc.param1); + for (int j = 0; j < base1CD->get_numtables(); j++) { + Table *tbl = base1CD->get_table(j); + if (LowerCase(tbl->get_name()) != tname) { + continue; + } + tbl->begin_edit(); + uint32_t total = tbl->get_phys_numrecords(); + uint32_t deleted = 0; + for (uint32_t i = 1; i < total; i++) { + TableRecord *rec = tbl->get_record(i); + bool removed = rec->is_removed(); + delete rec; + if (!removed) { + tbl->mark_record_removed(i); + deleted++; + } + } + base1CD->flush(); + msreg_g.AddMessage("Очистка таблицы выполнена.", MessageState::Succesfull) + .with("Таблица", tbl->get_name()) + .with("Удалено записей", (int)deleted) + .with("Физических записей всего", (int)total); + return; + } + msreg_g.AddError("Таблица для очистки не найдена.").with("Таблица", pc.param1); +} + +void App::delete_one_record(const ParsedCommand &pc) +{ + const string tname = LowerCase(pc.param1); + uint32_t idx = (uint32_t)strtoul(pc.param2.c_str(), nullptr, 10); + for (int j = 0; j < base1CD->get_numtables(); j++) { + Table *tbl = base1CD->get_table(j); + if (LowerCase(tbl->get_name()) != tname) { + continue; + } + tbl->begin_edit(); + tbl->mark_record_removed(idx); + base1CD->flush(); + msreg_g.AddMessage("Запись помечена удалённой.", MessageState::Succesfull) + .with("Таблица", tbl->get_name()) + .with("Физический номер записи", (int)idx); + return; + } + msreg_g.AddError("Таблица не найдена.").with("Таблица", pc.param1); +} + void App::save_config(const boost::filesystem::path& param_path) { if(!is_infobase()) { @@ -660,6 +712,14 @@ int App::Run() save_tables_sizes(pc); break; } + case Command::clear_table: { + clear_table(pc); + break; + } + case Command::delete_one_record: { + delete_one_record(pc); + break; + } } } catch (string &s) { diff --git a/src/ctool1cd/App.h b/src/ctool1cd/App.h index 63d61285..af096680 100644 --- a/src/ctool1cd/App.h +++ b/src/ctool1cd/App.h @@ -73,6 +73,10 @@ class App { void save_tables_sizes(const ParsedCommand& pc); + void clear_table(const ParsedCommand& pc); + + void delete_one_record(const ParsedCommand& pc); + inline bool is_infobase() const; }; diff --git a/src/ctool1cd/ParseCommandLine.cpp b/src/ctool1cd/ParseCommandLine.cpp index 7b5429bd..df336d6a 100755 --- a/src/ctool1cd/ParseCommandLine.cpp +++ b/src/ctool1cd/ParseCommandLine.cpp @@ -74,6 +74,10 @@ CommandDefinition CommandParse::definitions[] = {"savelostobjects", Command::find_and_save_lost_objects, 1, ""}, // 37 {"sts", Command::save_tables_size, 1, ""}, // 38 {"savetablessize", Command::save_tables_size, 1, ""}, // 39 + {"ct", Command::clear_table, 1, ""}, // 40 + {"cleartable", Command::clear_table, 1, ""}, // 41 + {"dr", Command::delete_one_record, 2, ""}, // 42 + {"deleterecord", Command::delete_one_record, 2, ""}, // 43 }; diff --git a/src/ctool1cd/ParseCommandLine.h b/src/ctool1cd/ParseCommandLine.h index cc485a5d..945815b8 100755 --- a/src/ctool1cd/ParseCommandLine.h +++ b/src/ctool1cd/ParseCommandLine.h @@ -47,6 +47,8 @@ enum class Command import_from_binary, // загрузить таблицы из двоичных файлов, выгруженных экспортом find_and_save_lost_objects, // найти и сохранить потерянные объекты save_tables_size, // сохранить в файл размеры таблиц + clear_table, // удалить все записи указанной таблицы + delete_one_record, // пометить удалённой одну запись таблицы по физическому индексу }; struct CommandDefinition diff --git a/src/gtool1cd/mainwindow.cpp b/src/gtool1cd/mainwindow.cpp index e5c64bcb..de03486d 100644 --- a/src/gtool1cd/mainwindow.cpp +++ b/src/gtool1cd/mainwindow.cpp @@ -31,6 +31,8 @@ #include "configurations_window.h" #include "about_dialog.h" #include +#include +#include void MainWindow::AddDetailedMessage( const std::string &description, @@ -70,7 +72,13 @@ void MainWindow::show_table_context_menu(const QPoint &pos) QAction action_import_blob(tr("Импорт BLOB"), this); connect(&action_import_blob, SIGNAL(triggered()), this, SLOT(import_blob_file())); contextMenu.addAction(&action_import_blob); - + + contextMenu.addSeparator(); + + QAction action_clear_table(tr("Очистить таблицу"), this); + connect(&action_clear_table, SIGNAL(triggered()), this, SLOT(clear_table_action())); + contextMenu.addAction(&action_clear_table); + contextMenu.exec(mapToGlobal(pos)); } @@ -109,6 +117,58 @@ void MainWindow::import_blob_file() } } +void MainWindow::clear_table_action() +{ + auto indexes = ui->tableListView->selectionModel()->selectedIndexes(); + if (indexes.empty()) { + return; + } + + auto *proxy = qobject_cast(ui->tableListView->model()); + QModelIndex src = proxy ? proxy->mapToSource(indexes.first()) : indexes.first(); + Table *t = db->get_table(src.row()); + + if (db->get_readonly()) { + QMessageBox::warning(this, tr("Очистка таблицы"), + tr("База открыта только для чтения — очистка невозможна.")); + return; + } + + if (QMessageBox::warning(this, tr("Очистка таблицы"), + tr("Пометить удалёнными ВСЕ записи таблицы «%1»?\nДействие изменяет базу и необратимо.") + .arg(QString::fromStdString(t->get_name())), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) { + return; + } + + uint32_t deleted = 0; + try { + t->begin_edit(); + uint32_t total = t->get_phys_numrecords(); + for (uint32_t i = 1; i < total; i++) { + auto *rec = t->get_record(i); + bool removed = rec->is_removed(); + delete rec; + if (!removed) { + t->mark_record_removed(i); + deleted++; + } + } + db->flush(); + } catch (DetailedException &ex) { + QMessageBox::critical(this, tr("Очистка таблицы"), QString(ex.what())); + return; + } + + auto it = table_windows.find(t); + if (it != table_windows.end()) { + static_cast(it.value())->reload(); + } + + QMessageBox::information(this, tr("Очистка таблицы"), + tr("Готово. Помечено удалёнными записей: %1.").arg(deleted)); +} + MainWindow::~MainWindow() { delete ui; diff --git a/src/gtool1cd/mainwindow.h b/src/gtool1cd/mainwindow.h index a89527ec..7e63574d 100644 --- a/src/gtool1cd/mainwindow.h +++ b/src/gtool1cd/mainwindow.h @@ -67,9 +67,11 @@ private slots: void show_table_context_menu(const QPoint &); void export_blob_file(); - + void import_blob_file(); + void clear_table_action(); + private: Ui::MainWindow *ui; T_1CD *db; diff --git a/src/gtool1cd/models/table_data_model.cpp b/src/gtool1cd/models/table_data_model.cpp index b9386b92..059e35b0 100644 --- a/src/gtool1cd/models/table_data_model.cpp +++ b/src/gtool1cd/models/table_data_model.cpp @@ -226,6 +226,19 @@ const TableRecord *TableDataModel::getRecord(const QModelIndex &index) const } +uint32_t TableDataModel::physicalRecordNo(const QModelIndex &index) const +{ + return _index == nullptr + ? index.row() + : _index->get_numrec(index.row()); +} + +void TableDataModel::notifyRowChanged(int row) +{ + emit dataChanged(index(row, 0, QModelIndex()), + index(row, columnCount(QModelIndex()) - 1, QModelIndex())); +} + TStream *TableDataModel::getBlobStream(const QModelIndex &index) const { Field *f = table->get_field(index.column()); diff --git a/src/gtool1cd/models/table_data_model.h b/src/gtool1cd/models/table_data_model.h index bc5e18e3..85c4e2da 100644 --- a/src/gtool1cd/models/table_data_model.h +++ b/src/gtool1cd/models/table_data_model.h @@ -53,6 +53,12 @@ class TableDataModel : public QAbstractItemModel const TableRecord *getRecord(const QModelIndex &index) const; + // Физический номер записи в таблице для строки модели + uint32_t physicalRecordNo(const QModelIndex &index) const; + + // Сообщить представлению, что строка изменилась (например, помечена удалённой) + void notifyRowChanged(int row); + TStream *getBlobStream(const QModelIndex &index) const; private: diff --git a/src/gtool1cd/table_data_window.cpp b/src/gtool1cd/table_data_window.cpp index 71d66734..56149a83 100644 --- a/src/gtool1cd/table_data_window.cpp +++ b/src/gtool1cd/table_data_window.cpp @@ -29,6 +29,8 @@ #include #include "models/skobka_tree_model.h" #include +#include +#include #include "models/v8catalog_tree_model.h" QString index_presentation(Index *index) @@ -81,6 +83,10 @@ TableDataWindow::TableDataWindow(QWidget *parent, Table *table) emit ui->indexChooseBox->activated(1); } + ui->dataView->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->dataView, SIGNAL(customContextMenuRequested(QPoint)), + this, SLOT(show_data_context_menu(QPoint))); + ui->dataView->setFocus(); } @@ -199,3 +205,54 @@ void TableDataWindow::on_saveBlobButton_clicked() auto model = static_cast(ui->dataView->model()); model->dumpBlob(index, filename); } + +void TableDataWindow::reload() +{ + on_indexChooseBox_activated(ui->indexChooseBox->currentIndex()); +} + +void TableDataWindow::show_data_context_menu(const QPoint &pos) +{ + if (!ui->dataView->currentIndex().isValid()) { + return; + } + QMenu menu(this); + QAction action_delete(tr("Удалить запись"), this); + connect(&action_delete, SIGNAL(triggered()), this, SLOT(delete_record_action())); + menu.addAction(&action_delete); + menu.exec(ui->dataView->mapToGlobal(pos)); +} + +void TableDataWindow::delete_record_action() +{ + auto index = ui->dataView->currentIndex(); + if (!index.isValid()) { + return; + } + auto *model = static_cast(ui->dataView->model()); + uint32_t phys_numrecord = model->physicalRecordNo(index); + + if (table->get_base()->get_readonly()) { + QMessageBox::warning(this, tr("Удаление записи"), + tr("База открыта только для чтения — удаление невозможно.")); + return; + } + + if (QMessageBox::warning(this, tr("Удаление записи"), + tr("Пометить запись №%1 таблицы «%2» удалённой?\nДействие изменяет базу.") + .arg(phys_numrecord).arg(QString::fromStdString(table->get_name())), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) { + return; + } + + try { + table->begin_edit(); + table->mark_record_removed(phys_numrecord); + table->get_base()->flush(); + } catch (DetailedException &ex) { + QMessageBox::critical(this, tr("Удаление записи"), QString(ex.what())); + return; + } + + model->notifyRowChanged(index.row()); +} diff --git a/src/gtool1cd/table_data_window.h b/src/gtool1cd/table_data_window.h index fb0145bd..c9ef2dbc 100644 --- a/src/gtool1cd/table_data_window.h +++ b/src/gtool1cd/table_data_window.h @@ -40,8 +40,14 @@ class TableDataWindow : public QMainWindow explicit TableDataWindow(QWidget *parent, Table *table); ~TableDataWindow(); + void reload(); // перечитать данные таблицы в текущем представлении + private slots: + void show_data_context_menu(const QPoint &pos); + + void delete_record_action(); + void on_descriptionButton_clicked(); void on_fieldsButton_clicked(); diff --git a/src/tests/tool1cd/test_parse_bom.cpp b/src/tests/tool1cd/test_parse_bom.cpp new file mode 100644 index 00000000..f750f8f1 --- /dev/null +++ b/src/tests/tool1cd/test_parse_bom.cpp @@ -0,0 +1,64 @@ +/* + test_project provides tests for Tool1CD library + Copyright © 2009-2017 awa + Copyright © 2017-2018 E8 Tools contributors + + This file is part of test_project. + + test_project is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + test_project is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with test_project. If not, see . +*/ +#include "../catch.hpp" +#include +#include +#include +#include +#include + +// Прерванное динамическое обновление оставляет в служебных файлах конфигурации +// (например, DynamicallyUpdated) метку порядка байтов UTF-8. Разбор такого +// потока не должен прерываться — BOM пропускается. +TEST_CASE( "Parse_tree пропускает UTF-8 BOM в начале потока", "[tool1cd][common][Parse_tree][BOM]" ) { + + std::string tree_source = "{1,2,{3,4}\n}\n"; + + GIVEN ("Поток дерева с UTF-8 BOM (EF BB BF) в начале") { + + auto src = TEncoding::UTF8->fromUtf8(tree_source); + decltype(src) with_bom; + with_bom.push_back(0xEF); + with_bom.push_back(0xBB); + with_bom.push_back(0xBF); + with_bom.insert(with_bom.end(), src.begin(), src.end()); + + WHEN ("Разбираем поток") { + + TBytesStream bs(std::move(with_bom)); + auto tree = parse_1Cstream(&bs, ""); + + THEN ("Дерево разбирается так же, как без BOM") { + + REQUIRE(tree != nullptr); + std::string out_with_bom; + tree->outtext(out_with_bom); + + auto reference = parse_1Ctext(tree_source, ""); + REQUIRE(reference != nullptr); + std::string out_reference; + reference->outtext(out_reference); + + REQUIRE(out_with_bom == out_reference); + } + } + } +} diff --git a/src/tests/tool1cd/test_write838.cpp b/src/tests/tool1cd/test_write838.cpp new file mode 100644 index 00000000..24010a29 --- /dev/null +++ b/src/tests/tool1cd/test_write838.cpp @@ -0,0 +1,204 @@ +/* + test_project provides tests for Tool1CD library + Copyright © 2009-2017 awa + Copyright © 2017-2018 E8 Tools contributors + + This file is part of test_project. + + test_project is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + test_project is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with test_project. If not, see . +*/ +#include "../catch.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using boost::filesystem::path; + +// Копирует эталонную базу из дерева исходников во временный файл, чтобы тесты +// записи не меняли фикстуру в репозитории. +static path copy_fixture(const std::string &relpath) +{ + path src = path(CMAKE_SOURCE_DIR) / relpath; + path dst = boost::filesystem::temp_directory_path() + / boost::filesystem::unique_path("tool1cd_%%%%%%%%.1CD"); + boost::filesystem::copy_file(src, dst); + return dst; +} + +// Первая таблица, у которой есть объект данных. +static Table *first_table_with_data(T_1CD &base) +{ + for (int i = 0; i < base.get_numtables(); i++) { + if (base.get_table(i)->get_file_data() != nullptr) { + return base.get_table(i); + } + } + return nullptr; +} + +static Table *table_by_name(T_1CD &base, const std::string &name) +{ + for (int i = 0; i < base.get_numtables(); i++) { + if (base.get_table(i)->get_name() == name) { + return base.get_table(i); + } + } + return nullptr; +} + +// Запись объекта в формате 8.3.8 с ростом числа блоков (fatlevel 0). +// Раньше при росте не обновлялось число блоков объекта и запись уходила +// в нулевой блок (заголовок базы) — здесь это проверяется явно. +TEST_CASE("set_data растит объект формата 8.3.8 и читает данные обратно", + "[tool1cd][V8Object][write][8.3.8]") +{ + path dbfile = copy_fixture("tests/db838/db01/1Cv8.1CD"); + + { + T_1CD base(dbfile, nullptr, true); + REQUIRE(base.is_open()); + + Table *t = first_table_with_data(base); + REQUIRE(t != nullptr); + + V8Object *fd = t->get_file_data(); + uint64_t old_len = fd->get_len(); + + // Гарантированно несколько блоков (fatlevel 0): +30 страниц по 8192 байта. + uint64_t new_len = old_len + 30ull * 8192ull; + std::vector pattern(new_len); + for (uint64_t i = 0; i < new_len; i++) { + pattern[i] = static_cast((i * 131 + 17) & 0xFF); + } + + WHEN("Записываем объект целиком большего размера") { + // Если число блоков не обновлено, запись попадёт в блок 0 и сработает + // защита get_block_for_write — set_data бросит исключение. + REQUIRE(fd->set_data(pattern.data(), new_len)); + + THEN("Длина объекта и его данные соответствуют записанным") { + REQUIRE(fd->get_len() == new_len); + + std::vector readback(new_len); + fd->get_data(readback.data(), 0, new_len); + REQUIRE(std::memcmp(readback.data(), pattern.data(), new_len) == 0); + } + } + } + + boost::filesystem::remove(dbfile); +} + +// set_version должна писать версию по смещению, верному для формата 8.3.8, +// и не затрагивать длину объекта (раньше версия писалась поверх длины). +TEST_CASE("set_version пишет версию и не затирает длину объекта (8.3.8)", + "[tool1cd][V8Object][write][8.3.8]") +{ + path dbfile = copy_fixture("tests/db838/db01/1Cv8.1CD"); + + std::string table_name; + uint64_t original_len = 0; + + { + T_1CD base(dbfile, nullptr, true); + REQUIRE(base.is_open()); + + Table *t = first_table_with_data(base); + REQUIRE(t != nullptr); + table_name = t->get_name(); + + V8Object *fd = t->get_file_data(); + original_len = fd->get_len(); + + fd->set_version(0x01020304, 0x05060708); + base.flush(); + } + + GIVEN("Переоткрытая после записи версии база") { + T_1CD reopened(dbfile, nullptr, true); + REQUIRE(reopened.is_open()); + + Table *t = table_by_name(reopened, table_name); + REQUIRE(t != nullptr); + V8Object *fd = t->get_file_data(); + + THEN("Длина объекта сохранилась, а версия записана корректно") { + REQUIRE(fd->get_len() == original_len); + + auto ver = fd->get_current_version(); + REQUIRE(ver.version_1 == 0x01020304); + REQUIRE(ver.version_2 == 0x05060708); + } + } + + boost::filesystem::remove(dbfile); +} + +// Пометка записи удалённой должна сохраняться при переоткрытии базы. +TEST_CASE("mark_record_removed помечает запись удалённой (8.3.8)", + "[tool1cd][Table][write][8.3.8]") +{ + path dbfile = copy_fixture("tests/db838/db01/1Cv8.1CD"); + + std::string table_name; + uint32_t record_no = 0; + + { + T_1CD base(dbfile, nullptr, true); + REQUIRE(base.is_open()); + + // Ищем первую неудалённую запись в любой таблице. + for (int i = 0; i < base.get_numtables() && record_no == 0; i++) { + Table *t = base.get_table(i); + uint32_t total = t->get_phys_numrecords(); + for (uint32_t r = 1; r < total; r++) { + TableRecord *rec = t->get_record(r); + bool removed = rec->is_removed(); + delete rec; + if (!removed) { + table_name = t->get_name(); + record_no = r; + t->begin_edit(); + t->mark_record_removed(r); + break; + } + } + } + REQUIRE(record_no != 0); + base.flush(); + } + + GIVEN("Переоткрытая после пометки база") { + T_1CD reopened(dbfile, nullptr, true); + REQUIRE(reopened.is_open()); + + Table *t = table_by_name(reopened, table_name); + REQUIRE(t != nullptr); + + THEN("Запись отмечена как удалённая") { + TableRecord *rec = t->get_record(record_no); + bool removed = rec->is_removed(); + delete rec; + REQUIRE(removed); + } + } + + boost::filesystem::remove(dbfile); +} diff --git a/src/tool1cd/Class_1CD.cpp b/src/tool1cd/Class_1CD.cpp index 5b05f562..428f897e 100755 --- a/src/tool1cd/Class_1CD.cpp +++ b/src/tool1cd/Class_1CD.cpp @@ -91,6 +91,10 @@ char* T_1CD::get_block_for_write(uint32_t block_number, bool read) if(!fs) return nullptr; + if(block_number == 0 && !read) + { + throw DetailedException("Попытка записи данных в блок 0 (заголовок базы)"); + } if(block_number > length) { throw DetailedException("Попытка получения блока за пределами файла базы.") diff --git a/src/tool1cd/ConfigStorage.cpp b/src/tool1cd/ConfigStorage.cpp index 81a66d8d..3ec4e3ed 100755 --- a/src/tool1cd/ConfigStorage.cpp +++ b/src/tool1cd/ConfigStorage.cpp @@ -19,6 +19,7 @@ along with Tool1CD Library. If not, see . */ #include +#include #include "ConfigStorage.h" #include "Common.h" @@ -593,6 +594,8 @@ ConfigStorageTableConfig::ConfigStorageTableConfig(TableFiles* tabf, T_1CD* _bas present = tabf->get_table()->get_base()->get_filename() + "\\config"; + bool nodynup = getenv("TOOL1CD_NODYNUP") != nullptr; + tab = tabf->get_table(); _DynamicallyUpdated = tabf->get_file("DynamicallyUpdated"); @@ -662,6 +665,8 @@ ConfigStorageTableConfig::ConfigStorageTableConfig(TableFiles* tabf, T_1CD* _bas name += ext; } + if(nodynup && dynno != -1) continue; + auto pfiles = files.find(LowerCase(name)); if (pfiles == files.end()) { @@ -742,6 +747,8 @@ ConfigStorageTableConfigSave::ConfigStorageTableConfigSave(TableFiles* tabc, Tab tab = tabc->get_table(); _DynamicallyUpdated = tabc->get_file("DynamicallyUpdated"); + bool nodynup = getenv("TOOL1CD_NODYNUP") != nullptr; + std::vector dynup; if(_DynamicallyUpdated) { @@ -823,6 +830,8 @@ ConfigStorageTableConfigSave::ConfigStorageTableConfigSave(TableFiles* tabc, Tab name.append(spoint).append(ext); } + if(nodynup && dynno != -1) continue; + auto pfiles = files.find(LowerCase(name)); if(pfiles == files.end()) { diff --git a/src/tool1cd/Parse_tree.cpp b/src/tool1cd/Parse_tree.cpp index b52466ef..4c320d73 100755 --- a/src/tool1cd/Parse_tree.cpp +++ b/src/tool1cd/Parse_tree.cpp @@ -344,6 +344,10 @@ Tree* parse_flow(flow_type source, const std::string &path) case '\r': case '\n': break; + case (char)0xEF: // UTF-8 BOM (EF BB BF) — пропускаем, как пробел + case (char)0xBB: + case (char)0xBF: + break; case '"': cur_value.clear(); state = state_type::s_string; diff --git a/src/tool1cd/Table.cpp b/src/tool1cd/Table.cpp index ae787345..3b5d0dfd 100755 --- a/src/tool1cd/Table.cpp +++ b/src/tool1cd/Table.cpp @@ -442,7 +442,7 @@ void Table::init(int32_t block_descr) if(file_data) { - if(phys_numrecords * recordlen != file_data->get_len()) + if((uint64_t)phys_numrecords * (uint64_t)recordlen != file_data->get_len()) { throw DetailedException("Длина таблицы не кратна длине записи.") .add_detail("Блок", to_hex_string(block_descr)) @@ -1208,9 +1208,7 @@ void Table::import_table(const boost::filesystem::path &path) descr_changed = true; } file_data->set_data(f); - ob = (v8ob*)base->get_block_for_write(file_data->get_block_number(), true); - ob->version.version_1 = root.data_version_1; - ob->version.version_2 = root.data_version_2; + file_data->set_version(root.data_version_1, root.data_version_2); delete f; } } @@ -1235,9 +1233,7 @@ void Table::import_table(const boost::filesystem::path &path) descr_changed = true; } file_blob->set_data(f); - ob = (v8ob*)base->get_block_for_write(file_blob->get_block_number(), true); - ob->version.version_1 = root.blob_version_1; - ob->version.version_2 = root.blob_version_2; + file_blob->set_version(root.blob_version_1, root.blob_version_2); delete f; } } @@ -1262,9 +1258,7 @@ void Table::import_table(const boost::filesystem::path &path) descr_changed = true; } file_index->set_data(f); - ob = (v8ob*)base->get_block_for_write(file_index->get_block_number(), true); - ob->version.version_1 = root.index_version_1; - ob->version.version_2 = root.index_version_2; + file_index->set_version(root.index_version_1, root.index_version_2); delete f; } } @@ -1286,9 +1280,7 @@ void Table::import_table(const boost::filesystem::path &path) if(fopen) { if(!descr_table) descr_table = new V8Object(base); // вообще, если descr_table == nullptr, то это огромная ошибка! - ob = (v8ob*)base->get_block_for_write(descr_table->get_block_number(), true); - ob->version.version_1 = root.descr_version_1; - ob->version.version_2 = root.descr_version_2; + descr_table->set_version(root.descr_version_1, root.descr_version_2); auto buf_size = f->GetSize(); char *buf = new char[buf_size]; @@ -2011,6 +2003,25 @@ void Table::delete_record(uint32_t phys_numrecord) delete rec; } +//--------------------------------------------------------------------------- +// Помечает запись удалённой на уровне данных (первый байт записи != 0), не трогая +// индексы и не разбирая поля. Применяется там, где штатное delete_record невозможно +// из-за неподдерживаемого ключа сортировки строковых полей (например, V8USERS). +// Индексные ссылки остаются, но указывают на удалённые записи и платформой пропускаются; +// при необходимости индексы перестраиваются "Тестированием и исправлением". +void Table::mark_record_removed(uint32_t phys_numrecord) +{ + if(!edit) + { + throw DetailedException("Попытка пометки записи удалённой не в режиме редактирования.") + .add_detail("Таблица", name); + } + if(!file_data) return; + if(phys_numrecord == 0 || phys_numrecord >= phys_numrecords) return; + unsigned char removed_flag = 1; + file_data->set_data(&removed_flag, (uint64_t)phys_numrecord * recordlen, 1); +} + //--------------------------------------------------------------------------- void Table::insert_record(const TableRecord *nrec) { diff --git a/src/tool1cd/Table.h b/src/tool1cd/Table.h index 3d96612e..7f4f8bfd 100644 --- a/src/tool1cd/Table.h +++ b/src/tool1cd/Table.h @@ -176,6 +176,7 @@ class Table { void import_table(const boost::filesystem::path &path); void delete_record(uint32_t phys_numrecord); // удаление записи + void mark_record_removed(uint32_t phys_numrecord); // пометить запись удалённой (первый байт != 0) без обновления индексов void insert_record(const TableRecord *rec); // добавление записи void update_record(uint32_t phys_numrecord, char* rec, char* changed_fields); // изменение записи char* get_record_template_test(); diff --git a/src/tool1cd/V8Object.cpp b/src/tool1cd/V8Object.cpp index 163aa01e..824f10ad 100755 --- a/src/tool1cd/V8Object.cpp +++ b/src/tool1cd/V8Object.cpp @@ -922,6 +922,11 @@ bool V8Object::set_data(TStream* stream) if(curoffobjblock >= offsperpage) { curoffobjblock = 0; + if (curobjblock >= blocks.size()) { + throw DetailedException("Выход за пределы таблицы размещения при записи объекта") + .add_detail("curobjblock", (int)curobjblock) + .add_detail("blocks_size", (int)blocks.size()); + } bb = (objtab838*) base->get_block(blocks[curobjblock++]); } } @@ -1136,7 +1141,7 @@ void V8Object::set_len(uint64_t _len) len = _len; if(numblocks > 0) { std::copy(std::begin(b->blocks), - std::begin(b->blocks) + (numblocks * 4), + std::begin(b->blocks) + numblocks, blocks.begin()); } @@ -1181,13 +1186,17 @@ void V8Object::set_len(uint64_t _len) // Увеличение длины объекта if(fatlevel == 0 && newfatlevel) { - bl = base->get_free_block(); - bb = (objtab838*)base->get_block_for_write(bl, false); - memcpy(bb->blocks, bd->blocks, numblocks * 4); fatlevel = newfatlevel; bd->fatlevel = newfatlevel; - bd->blocks[0] = bl; - numblocks = 1; + if(numblocks > 0) + { + bl = base->get_free_block(); + bb = (objtab838*)base->get_block_for_write(bl, false); + memcpy(bb->blocks, bd->blocks, numblocks * 4); + bd->blocks[0] = bl; + numblocks = 1; + } + else numblocks = 0; // пустой объект: первую страницу размещения выделит цикл ниже } else bb = (objtab838*)base->get_block_for_write(bd->blocks[numblocks - 1], true); @@ -1222,9 +1231,10 @@ void V8Object::set_len(uint64_t _len) // Уменьшение длины объекта if(fatlevel) { - bb = (objtab838*)base->get_block_for_write(b->blocks[numblocks - 1], true); - for(cur_data_blocks--; cur_data_blocks >= num_data_blocks; cur_data_blocks--) + bb = (objtab838*)base->get_block_for_write(bd->blocks[numblocks - 1], true); + while(cur_data_blocks > num_data_blocks) { + cur_data_blocks--; i = cur_data_blocks % offsperpage; base->set_block_as_free(bb->blocks[i]); bb->blocks[i] = 0; @@ -1232,14 +1242,15 @@ void V8Object::set_len(uint64_t _len) { base->set_block_as_free(bd->blocks[--numblocks]); bd->blocks[numblocks] = 0; - if(numblocks) bb = (objtab838*)base->get_block_for_write(b->blocks[numblocks - 1], true); + if(numblocks) bb = (objtab838*)base->get_block_for_write(bd->blocks[numblocks - 1], true); } } } else { - for(cur_data_blocks--; cur_data_blocks >= num_data_blocks; cur_data_blocks--) + while(cur_data_blocks > num_data_blocks) { + cur_data_blocks--; base->set_block_as_free(bd->blocks[cur_data_blocks]); bd->blocks[cur_data_blocks] = 0; } @@ -1261,9 +1272,10 @@ void V8Object::set_len(uint64_t _len) } len = _len; + numblocks = num_blocks; if(numblocks > 0) { std::copy(std::begin(bd->blocks), - std::begin(bd->blocks) + (numblocks * 4), + std::begin(bd->blocks) + numblocks, blocks.begin()); } @@ -1281,6 +1293,15 @@ void V8Object::set_block_as_free(uint32_t block_number) .add_detail("Блок объекта", block); } + if(type == v8objtype::free838) + { + // Структура списка свободных блоков формата 8.3.8 не реализована для записи. + // Освобождаемые блоки оставляем "осиротевшими": объект свободных блоков (блок 1) не меняем, + // поэтому он остаётся валидным. Неиспользуемые блоки можно вернуть штатным + // "Тестированием и исправлением" платформы. Это безопаснее, чем писать в непонятую структуру. + return; + } + uint32_t j = len >> 10; // length / 1024 int32_t i = len & 0x3ff; // length % 1024 @@ -1301,7 +1322,7 @@ void V8Object::set_block_as_free(uint32_t block_number) real_numblocks++; blocks.resize(real_numblocks); std::copy(std::begin(ob->blocks), - std::begin(ob->blocks) + (real_numblocks * 4), + std::begin(ob->blocks) + real_numblocks, blocks.begin()); } @@ -1382,6 +1403,19 @@ void V8Object::write_new_version() new_version_recorded = true; } +//--------------------------------------------------------------------------- +void V8Object::set_version(uint32_t v1, uint32_t v2) +{ + version.version_1 = v1; + version.version_2 = v2; + int32_t veroffset = type == v8objtype::data80 || type == v8objtype::free80 ? 12 : 4; + _version nv; + nv.version_1 = v1; + nv.version_2 = v2; + memcpy(base->get_block_for_write(block, true) + veroffset, &nv, 8); + new_version_recorded = true; +} + _version V8Object::get_current_version() const { return version; diff --git a/src/tool1cd/V8Object.h b/src/tool1cd/V8Object.h index f733bb7d..0bcb2b73 100644 --- a/src/tool1cd/V8Object.h +++ b/src/tool1cd/V8Object.h @@ -121,6 +121,7 @@ class V8Object { void get_version_rec_and_increase(_version* ver); // получает версию очередной записи и увеличивает сохраненную версию объекта void get_version(_version* ver); // получает сохраненную версию объекта void write_new_version(); // записывает новую версию объекта + void set_version(uint32_t v1, uint32_t v2); // устанавливает конкретную версию объекта по верному для формата смещению _version get_current_version() const; V8Object* get_next();