#include #include #include #include #include #include #include namespace LOTRO_DAT { /*! * \author Gi1dor * \date 30.06.2018 * Конструктор объекта модуля. Вызывается только из класса DatFile. Создаёт указатель но объект DatFile, но * не инициализирует модуль. Инизиализация происходит в функции Init * \param[in] datFilePtr Указатель на объект управляющего класса DatFile */ DatFileSystem::DatFileSystem(DatFile *datFilePtr) : dat(datFilePtr) {} /*! * \author Gi1dor * \date 29.06.2018 * Возвращает бинарные данные файла. Помимо самого файла, в бинарном содержимом находится также метаинформация о нём. * Изменение параметра offset позволяет игнорировать метаинформацию и извлечь только непосредственно содержимое файла * \param[in] file_id Идентификатор получаемого файла * \param[in] offset Отступ от начала файла. Данные до отступа не извлекаются. * \return Возвращает result = ERROR и value = BinaryData, если файл не найден или информаци в словаре некорректная */ DatOperationResult DatFileSystem::GetFileData(const SubFile& file, long long int offset) { if (!dat) return DatOperationResult(BinaryData(), ERROR, "DatFileSystem error: no connection with Dat (dat is nullptr)"); BinaryData mfile_id(20); auto operation = CheckCorrectSubfile(file); if (operation.result == ERROR || !operation.value) return DatOperationResult(BinaryData(), ERROR, "DATFSGETFILEDATA: Incorrect file" + std::to_string(file.file_id())); BinaryData data((unsigned) (file.file_size() + (8 - offset))); if (file.block_size() >= file.file_size() + 8) { dat->GetIO().ReadData(data, file.file_size() + (8 - offset), file.file_offset() + offset); return DatOperationResult(data, SUCCESS); } BinaryData fragments_count(4); dat->GetIO().ReadData(fragments_count, 4, file.file_offset()); long long fragments_number = fragments_count.ToNumber<4>(0); long long current_block_size = file.block_size() - offset - 8 * fragments_number; dat->GetIO().ReadData(data, current_block_size, file.file_offset() + offset); BinaryData FragmentsDictionary(8 * unsigned(fragments_number)); dat->GetIO().ReadData(FragmentsDictionary, 8 * unsigned(fragments_number), file.file_offset() + file.block_size() - 8 * fragments_number); for (long long i = 0; i < fragments_number; i++) { long long fragment_size = FragmentsDictionary.ToNumber<4>(8 * i); long long fragment_offset = FragmentsDictionary.ToNumber<4>(8 * i + 4); dat->GetIO().ReadData(data, std::min(fragment_size, file.file_size() - current_block_size), fragment_offset, current_block_size); current_block_size += fragment_size; } return DatOperationResult(data, SUCCESS); } /*! * \author Gi1dor * \date 29.06.2018 * Возвращает указатель на объект файла в словаре. Через этот указатель можно патчить/извлекать файл * \param[in] file_id идентификатор получаемого файла * \return Возвращает result = ERROR и value = nullptr, если файл не найден */ DatOperationResult> DatFileSystem::GetFile(long long file_id) { if (!dat) return DatOperationResult>(nullptr, ERROR, "DATFSGETFILE: no connection with Dat (dat is nullptr)"); if (dictionary_.count(file_id) == 0) { auto operation = InitSubFile(file_id); if (operation.result == ERROR) return DatOperationResult>(nullptr, ERROR); } return DatOperationResult>(dictionary_[file_id], SUCCESS); } /*! * \author Gi1dor * \date 29.06.2018 * Обновляет данные файла * \param[in] preinit_file неинициализированный объект файла, данные которого обновляются в словаре * \warning Новые данные не будут записаны в dat файле, пока не будет вызвана функция CommitDirectories() */ DatOperationResult<> DatFileSystem::UpdateFileInfo(const SubFile &preinit_file) { auto operation = GetFile(preinit_file.file_id()); if (operation.result == ERROR) return DatOperationResult<>(ERROR, "DATFS: Unable to update file info - no file in dict with id = " + std::to_string(preinit_file.file_id())); auto& file = operation.value; file->unknown1_ = preinit_file.unknown1(); file->file_id_ = preinit_file.file_id(); file->file_offset_ = preinit_file.file_offset(); file->file_size_ = preinit_file.file_size(); file->timestamp_ = preinit_file.timestamp(); file->version_ = preinit_file.version(); file->block_size_ = preinit_file.block_size(); file->unknown2_ = preinit_file.unknown2(); subfile_pending_update.insert(preinit_file.file_id()); return DatOperationResult<>(); } /*! * \author Gi1dor * \date 29.06.2018 * Проверяет корректность файла, полученного из словаря * * \return Объект DatOperationResult для которого value = true, если файл корректный, и false - иначе. */ DatOperationResult DatFileSystem::CheckCorrectSubfile(const SubFile& file) { if (!dat) return DatOperationResult(false, ERROR, "DATFSCORRECTSUBFILE: no connection with Dat (dat is nullptr)"); if (file.file_size() < 16) return DatOperationResult(false, SUCCESS); BinaryData mfile_id(20); auto operation = dat->GetIO().ReadData(mfile_id, 20, file.file_offset() + 8); if (operation.result == ERROR) return DatOperationResult(false, ERROR, "DATFSCORRECTSUBFILE: cannot read file header data"); return DatOperationResult((mfile_id.CheckCompression() || file.file_id() == mfile_id.ToNumber<4>(0)) && file.file_size() < 50ll * 1024ll * 1024ll, SUCCESS); } /*! * \author Gi1dor * \date 29.06.2018 * Записывает все изменения структуры файловой системы в dat-файл. */ DatOperationResult<> DatFileSystem::CommitDirectories() { if (!dat) return DatOperationResult<>(ERROR, "DATFSCOMMITDIRS: no connection with Dat (dat is nullptr)"); for (auto file_id : subfile_pending_update) { if (dictionary_.count(file_id) == 0) continue; // auto operation = CheckCorrectSubfile(dictionary_[file_id]); // if (operation.result == ERROR) { // LOG(ERROR) << "Check subfile correctness failed"; // continue; // } // if (!operation.value) { // LOG(ERROR) << "Incorrect SubFile " << file_id << " data: doesn't match"; // continue; // } auto operation1 = dat->GetIO().WriteData(dictionary_[file_id]->MakeHeaderData(), 32, dictionary_[file_id]->dictionary_offset()); if (operation1.result == ERROR) LOG(ERROR) << "Unable to write data to dictionary for file " << file_id; } subfile_pending_update.clear(); return DatOperationResult<>(SUCCESS); } /*! * \author Gi1dor * \date 29.06.2018 * Инициализация модуля. Подготавливает файловую систему для работы */ DatOperationResult<> DatFileSystem::Init() { if (!dat) return DatOperationResult<>(ERROR, "DATFSINIT: no connection with Dat (dat is nullptr)"); DeInit(); patched_file_end = dat->GetIO().file_size; SubDirectory root_directory(0, (unsigned) dat->GetIO().root_directory_offset); subdir_init_queue_.insert(root_directory); return DatOperationResult<>(SUCCESS); } /*! * \author Gi1dor * \date 29.06.2018 * Завершает работу файловой системы и записывает изменения в dat файл * * \return Вернёт DatOperationResult<>(ERROR), если случилась ошибка записи структуры файлов в dat файл */ DatOperationResult<> DatFileSystem::DeInit() { if (!dat) return DatOperationResult<>(ERROR, "DATFSDEINIT: no connection with Dat (dat is nullptr)"); auto operation = CommitDirectories(); dictionary_.clear(); visited_subdirectories_offsets_.clear(); visited_subfiles_ids_.clear(); subdir_init_queue_.clear(); subfile_init_map_.clear(); subfile_init_queue_.clear(); subfile_pending_update.clear(); if (operation.result == ERROR) return DatOperationResult<>(ERROR, "DEINITFS: unable to commit directories."); return DatOperationResult<>(SUCCESS); } /*! * \author Gi1dor * \date 29.06.2018 * Записывает общую информацию о файловой системе. Требует полной инициализации всех папок и файлов. * * \param[in] file Объект файла, в конец которого будут записываться данные. */ void DatFileSystem::PrintInformaion(FILE *file) { if (!dat) { LOG(ERROR) << "DATFSPRINTINFO: no connection with Dat (dat is nullptr)"; return; } InitAllFiles(); fprintf(file, "============= FS INFO SECTION =============\n"); fprintf(file, "Files in dictionary number: %d\n", dictionary_.size()); fprintf(file, "Files visited: %d\n", visited_subfiles_ids_.size()); fprintf(file, "Folders visited: %d\n", visited_subdirectories_offsets_.size()); fprintf(file, "File patched size: %u\n", patched_file_end); std::map filetypes_count{{TEXT, 0}, {JPG, 0}, {DDS, 0}, {WAV, 0}, {OGG, 0}, {FONT, 0}, {UNKNOWN, 0}}; for (const auto &datfile : dictionary_) { filetypes_count[datfile.second->FileType()]++; } fprintf(file, "TEXT files number = %d\n", filetypes_count[TEXT]); fprintf(file, "JPG files number = %d\n", filetypes_count[JPG]); fprintf(file, "DDS files number = %d\n", filetypes_count[DDS]); fprintf(file, "WAV files number = %d\n", filetypes_count[WAV]); fprintf(file, "OGG files number = %d\n", filetypes_count[OGG]); fprintf(file, "FONT files number = %d\n", filetypes_count[FONT]); fprintf(file, "UNKNOWN files number = %d\n", filetypes_count[UNKNOWN]); } /*! * \author Gi1dor * \date 29.06.2018 * Инициализация файла с заданным id. Ищет информацию о файле, инициализирует его в соответствии с типом файла и * помещает в словарь. Работает быстрее, если все папки уже были проинициализированы. В противном случае - пытается * инициализировать каждую из папок, пока не найдёт в ней файла с требуем id. * * \param[in] file_id Id файла, который нужно проинициализировать. * \return Объект DatOperationResult. Если файл не удалось проинициализировать - поле result будет равно ERROR */ DatOperationResult<> DatFileSystem::InitSubFile(long long file_id) { if (!dat) return DatOperationResult<>(ERROR, "DATFSINITSUBFILE: no connection with Dat (dat is nullptr)"); while (subfile_init_map_.count(file_id) == 0) { if (subdir_init_queue_.empty()) return DatOperationResult<>(ERROR, "DATFSINITSUBFILE: Cannot init file with id = " + std::to_string(file_id) + " (NOT FOUND)"); SubDirectory dir = *subdir_init_queue_.begin(); subdir_init_queue_.erase(subdir_init_queue_.begin()); InitSubDirectory(dir); } SubFile file = subfile_init_map_[file_id]; subfile_init_queue_.erase(file); subfile_init_map_.erase(file_id); auto initialised_file = SubFile::MakeSubfile(*dat, file); if (!initialised_file) return DatOperationResult<>(ERROR, "DATFSINITSUBFILE: initialised subfile pointer is empty"); if (!CheckCorrectSubfile(*initialised_file).value) return DatOperationResult<>(ERROR, "DATFSINITSUBFILE: initialised file " + std::to_string(file.file_id()) + " is incorrect"); if (dictionary_.count(file_id) > 0) { LOG(WARNING) << "Dublicate files id = " << file_id << "dictionary offsets = " << dictionary_[file_id]->dictionary_offset() << " and " << initialised_file->dictionary_offset(); if (CheckCorrectSubfile(*dictionary_[file_id]).value) LOG(ERROR) << " FILE IN DICTIONARY IS ALREADY CORRECT!"; dictionary_[file_id] = initialised_file; } if (dictionary_.count(file_id) == 0) dictionary_[file_id] = initialised_file; return DatOperationResult<>(); } /*! * \author Gi1dor * \date 29.06.2018 * Инициализация всех папок. Собирает информацию обо всех файлах в .dat файле для быстрого доступа к ним. * \warning Сами файлы не инициализируются. Инициализация файла выполнится при первом вызове GetFile() или GetFileData() * * \return Объект DatOperationResult */ DatOperationResult<> DatFileSystem::InitAllDirectories() { if (!dat) return DatOperationResult<>(ERROR, "DATFSINITALLDIRS: no connection with Dat (dat is nullptr)"); while (!subdir_init_queue_.empty()) { SubDirectory dir = *subdir_init_queue_.begin(); subdir_init_queue_.erase(subdir_init_queue_.begin()); InitSubDirectory(dir); } return DatOperationResult<>(); } /*! * \author Gi1dor * \date 29.06.2018 * Инициализация всех файлов. Полностью инициализирует все внутренние папки и файлы и помещает последние в словарь. * * \return Объект DatOperationResult */ DatOperationResult<> DatFileSystem::InitAllFiles() { if (!dat) return DatOperationResult<>(ERROR, "DATFSINITALLFILES: no connection with Dat (dat is nullptr)"); InitAllDirectories(); while (!subfile_init_queue_.empty()) { SubFile file = *subfile_init_queue_.begin(); InitSubFile(file.file_id()); } return DatOperationResult<>(); } /*! * \author Gi1dor * \date 29.06.2018 * Инициализация папки. Читает внутреннюю структуру папки и получает информацию о файлах и подпапках. * * Структура папки: * 63 * 8 байт под описание подпапок. По 8 байт на папку. * Из них по 4 байта на 2 поля: * unknown - неизвестное поле * offset - положение подпапки в .dat файле * * 4 байта - количество файлов в папке. В каждой папке не более 64 Файлов. * * 32 * 64 байта под описание файлов. По 32 байта на каждый файл. * * Из них по 4 байта на каждое из 8 полей: * unknown1 - неизвестное поле * file_id - уникальный идентификатор файла * file_offset - положение содержимого файла * file_size - размер файла в байтах * timestamp - timestamp последнего редактирования * version - версия файла * block_size - размер блока данных, выделенного под файл * unknown2 - неизвестное поле * * \param[in] dir объект папки SubDirectory * \return Объект DatOperationResult */ DatOperationResult<> DatFileSystem::InitSubDirectory(SubDirectory dir) { if (!dat) return DatOperationResult<>(ERROR, "DATFSINITSUBDIR: no connection with Dat (dat is nullptr)"); BinaryData subdir_data(63 * 8 + 4 + 32 * 64); auto operation = dat->GetIO().ReadData(subdir_data, 63 * 8 + 4 + 32 * 64, dir.offset()); if (operation.result == ERROR) return DatOperationResult<>(ERROR, "DATFSINITSUBDIR: cannot init dir (read error) with offset = " + std::to_string((dir.offset()))); if (subdir_data.ToNumber<4>(0) != 0 || subdir_data.ToNumber<4>(4) != 0) return DatOperationResult<>(ERROR, "DATFSINITSUBDIR: incorrect dir. Nulls must be at first 8 bytes of dir"); // Subfiles section long long subfiles_number = subdir_data.ToNumber<4>(63 * 8); if (subfiles_number > 64) return DatOperationResult<>(ERROR, "DATINITSUBDIR: incorrect subdir: files number > 64"); // Subdirs section for (unsigned i = 8; i < 63 * 8; i += 8) { if (subdir_data.ToNumber<4>(i) == 0 || subdir_data.ToNumber<4>(i + 4) == 0) break; long long unknown = subdir_data.ToNumber<4>(i); long long offset = subdir_data.ToNumber<4>(i + 4); if (unknown == 0 || offset == 0) break; if (visited_subdirectories_offsets_.count(offset) != 0) continue; visited_subdirectories_offsets_.insert(offset); subdir_init_queue_.insert(SubDirectory(unknown, offset)); } // Subfiles section for (int i = 0; i < subfiles_number; i++) { long long header_start = 63 * 8 + 4 + 32 * i; SubFile file = SubFile( *dat, dir.offset() + header_start, subdir_data.ToNumber<4>(header_start + 0), // unknown1 subdir_data.ToNumber<4>(header_start + 4), // file_id subdir_data.ToNumber<4>(header_start + 8), // file_offset subdir_data.ToNumber<4>(header_start + 12), // file_size subdir_data.ToNumber<4>(header_start + 16), // timestamp subdir_data.ToNumber<4>(header_start + 20), // version subdir_data.ToNumber<4>(header_start + 24), // block_size subdir_data.ToNumber<4>(header_start + 28) // unknown2 ); if (file.version() == 0 || file.unknown2() != 0) continue; if (visited_subfiles_ids_.count(file.file_id()) || file.file_id() == -1) break; visited_subfiles_ids_.insert(file.file_id()); subfile_init_map_[file.file_id_] = file; subfile_init_queue_.insert(file); } return DatOperationResult<>(); } /*! * \author Gi1dor * \date 03.07.2018 * Выполнение функции function для всех файлов в dat контейнере. * * \param[in] function Функтор, принимающий в качестве аргумента ссылку на объект-указатель std::shared_ptr */ DatOperationResult<> DatFileSystem::PerformOperationOnAllFiles(const std::function&)>& function) { InitAllFiles(); for (auto& file_pair: dictionary_) function(file_pair.second); return DatOperationResult<>(); } int DatFileSystem::GetInitialisedFilesNumber() const { return dictionary_.size(); } }