diff --git a/.github/workflows/cmake-single-platform.yml b/.github/workflows/cmake-single-platform.yml index 1a566dd..5e9e9d3 100644 --- a/.github/workflows/cmake-single-platform.yml +++ b/.github/workflows/cmake-single-platform.yml @@ -16,9 +16,13 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Install System Dependencies + run: | + sudo apt-get update + sudo apt-get install -y clang-format pkg-config libzip-dev + - name: Check Code Formatting run: | - sudo apt-get update && sudo apt-get install -y clang-format find core cli -type f \( -name "*.cpp" -o -name "*.hpp" -o -name "*.h" \) | xargs -r clang-format --dry-run --Werror - name: Configure CMake diff --git a/cli/main.cpp b/cli/main.cpp index 0314ff1..185e580 100644 --- a/cli/main.cpp +++ b/cli/main.cpp @@ -1 +1,81 @@ -int main(int argc, char **argv) { return 0; } +#include "retroforge/bootstrap/CompressionEngineFactory.hpp" +#include "retroforge/bootstrap/IdentificationEngineFactory.hpp" +#include "retroforge/models/ConversionJob.hpp" +#include +#include +#include + +using namespace retroforge; + +std::string nome_do_console(models::Console c) { + switch (c) { + case models::Console::PS1: + return "PS1"; + case models::Console::PS2: + return "PS2"; + case models::Console::PSP: + return "PSP"; + case models::Console::PSP_PBP: + return "PSP (EBOOT.PBP)"; + case models::Console::GameCube: + return "GameCube"; + case models::Console::Dreamcast: + return "Dreamcast (Limpo)"; + case models::Console::DreamcastModificado: + return "Dreamcast (Modificado)"; + case models::Console::Genesis: + return "Mega Drive / Genesis"; + case models::Console::SNES: + return "SNES"; + case models::Console::SegaCD: + return "Sega CD"; + case models::Console::Saturn: + return "Sega Saturn"; + case models::Console::Desconhecido: + return "Desconhecido"; + default: + return "Outro"; + } +} + +int main() { + auto identificador = bootstrap::IdentificationEngineFactory::criar(); + auto roteador = bootstrap::CompressionEngineFactory::criar(); + + std::vector arquivos_para_testar = {}; + + std::cout << "============================================\n"; + std::cout << " PROTÓTIPO FUNCIONAL - MAIN RETROFORGE \n"; + std::cout << "============================================\n\n"; + + for (const auto &caminho : arquivos_para_testar) { + std::cout << "[ARQUIVO] " << caminho << "\n"; + + auto console = identificador.identify(caminho); + std::cout << " -> Identificado como: " << nome_do_console(console) << "\n"; + + if (console == models::Console::Desconhecido) { + std::cout << " -> Ignorando arquivo. Nao reconhecido pelos detectores.\n\n"; + continue; + } + + auto job = std::make_shared(caminho, console); + auto resultado = roteador.rotear(job); + + std::cout << " -> Status do Roteador: "; + switch (resultado) { + case compression::ResultadoRoteamento::ConversaoIniciada: + std::cout << "Conversao iniciada/concluida com sucesso.\n"; + break; + case compression::ResultadoRoteamento::BloqueadoPorSeguranca: + std::cout << "BLOQUEADO por seguranca (Trava do Smart Context ativada).\n"; + break; + case compression::ResultadoRoteamento::ConsoleNaoSuportado: + std::cout << "Falha na compressao ou sem rota registrada.\n"; + break; + } + std::cout << "\n"; + } + + return 0; +} \ No newline at end of file diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 2e19eea..b68a6f0 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -1,3 +1,14 @@ +if(UNIX AND NOT APPLE) + find_package(PkgConfig REQUIRED) + pkg_check_modules(LIBZIP REQUIRED IMPORTED_TARGET libzip) + + if(TARGET PkgConfig::LIBZIP AND NOT TARGET libzip::zip) + add_library(libzip::zip ALIAS PkgConfig::LIBZIP) + endif() +else() + find_package(libzip CONFIG REQUIRED) +endif() + file(GLOB_RECURSE CORE_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp" @@ -25,4 +36,8 @@ target_include_directories(retroforge_core PUBLIC ${CMAKE_BINARY_DIR}/generated ) -target_compile_features(retroforge_core PUBLIC cxx_std_20) \ No newline at end of file +target_compile_features(retroforge_core PUBLIC cxx_std_20) + +target_link_libraries(retroforge_core PRIVATE + libzip::zip +) \ No newline at end of file diff --git a/core/include/retroforge/bootstrap/CompressionEngineFactory.hpp b/core/include/retroforge/bootstrap/CompressionEngineFactory.hpp new file mode 100644 index 0000000..1246efb --- /dev/null +++ b/core/include/retroforge/bootstrap/CompressionEngineFactory.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "retroforge/compression/CompressionRouter.hpp" + +namespace retroforge::bootstrap { + +class CompressionEngineFactory { + public: + static compression::CompressionRouter criar(); +}; + +} // namespace retroforge::bootstrap \ No newline at end of file diff --git a/core/include/retroforge/compression/CompressionRouter.hpp b/core/include/retroforge/compression/CompressionRouter.hpp new file mode 100644 index 0000000..64f048b --- /dev/null +++ b/core/include/retroforge/compression/CompressionRouter.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include "retroforge/compression/ICompressionStrategy.hpp" +#include "retroforge/models/Console.hpp" +#include "retroforge/models/ConversionJob.hpp" +#include +#include + +namespace retroforge::compression { + +enum class ResultadoRoteamento { ConversaoIniciada, BloqueadoPorSeguranca, ConsoleNaoSuportado }; + +class CompressionRouter { + public: + void register_strategy(models::Console console, + std::shared_ptr estrategia); + ResultadoRoteamento rotear(const std::shared_ptr &job); + + private: + std::unordered_map> estrategias_; +}; + +} // namespace retroforge::compression \ No newline at end of file diff --git a/core/include/retroforge/compression/ICompressionStrategy.hpp b/core/include/retroforge/compression/ICompressionStrategy.hpp new file mode 100644 index 0000000..5075d3a --- /dev/null +++ b/core/include/retroforge/compression/ICompressionStrategy.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "retroforge/models/ConversionJob.hpp" + +#include + +namespace retroforge::compression { + +class ICompressionStrategy { + public: + virtual ~ICompressionStrategy() = default; + + virtual bool comprimir(const std::shared_ptr &job) = 0; + + virtual std::string extensao_saida() const = 0; +}; + +} // namespace retroforge::compression \ No newline at end of file diff --git a/core/include/retroforge/compression/converters/ChdmanConverter.hpp b/core/include/retroforge/compression/converters/ChdmanConverter.hpp new file mode 100644 index 0000000..9d60036 --- /dev/null +++ b/core/include/retroforge/compression/converters/ChdmanConverter.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include "retroforge/compression/ICompressionStrategy.hpp" + +namespace retroforge::compression::converters { + +class ChdmanConverter : public ICompressionStrategy { + public: + bool comprimir(const std::shared_ptr &job) override; + std::string extensao_saida() const override { return ".chd"; } +}; + +} // namespace retroforge::compression::converters \ No newline at end of file diff --git a/core/include/retroforge/compression/converters/DolphinToolConverter.hpp b/core/include/retroforge/compression/converters/DolphinToolConverter.hpp new file mode 100644 index 0000000..44ea951 --- /dev/null +++ b/core/include/retroforge/compression/converters/DolphinToolConverter.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include "retroforge/compression/ICompressionStrategy.hpp" + +namespace retroforge::compression::converters { + +class DolphinToolConverter : public ICompressionStrategy { + public: + bool comprimir(const std::shared_ptr &job) override; + std::string extensao_saida() const override { return ".rvz"; } +}; + +} // namespace retroforge::compression::converters \ No newline at end of file diff --git a/core/include/retroforge/compression/converters/LibzipPacker.hpp b/core/include/retroforge/compression/converters/LibzipPacker.hpp new file mode 100644 index 0000000..f8cadd5 --- /dev/null +++ b/core/include/retroforge/compression/converters/LibzipPacker.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include "retroforge/compression/ICompressionStrategy.hpp" + +namespace retroforge::compression::converters { + +class LibzipPacker : public ICompressionStrategy { + public: + bool comprimir(const std::shared_ptr &job) override; + std::string extensao_saida() const override { return ".zip"; } +}; + +} // namespace retroforge::compression::converters \ No newline at end of file diff --git a/core/include/retroforge/compression/converters/MaxcsoConverter.hpp b/core/include/retroforge/compression/converters/MaxcsoConverter.hpp new file mode 100644 index 0000000..6b777c0 --- /dev/null +++ b/core/include/retroforge/compression/converters/MaxcsoConverter.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include "retroforge/compression/ICompressionStrategy.hpp" + +namespace retroforge::compression::converters { + +class MaxcsoConverter : public ICompressionStrategy { + public: + bool comprimir(const std::shared_ptr &job) override; + std::string extensao_saida() const override { return ".cso"; } +}; + +} // namespace retroforge::compression::converters \ No newline at end of file diff --git a/core/include/retroforge/io/BinaryFileReader.hpp b/core/include/retroforge/io/BinaryFileReader.hpp index 1351f35..c1b3fed 100644 --- a/core/include/retroforge/io/BinaryFileReader.hpp +++ b/core/include/retroforge/io/BinaryFileReader.hpp @@ -22,4 +22,5 @@ class BinaryFileReader { std::ifstream arquivo_; std::uintmax_t tamanho_total_; }; + } // namespace retroforge::io diff --git a/core/include/retroforge/models/ConversionJob.hpp b/core/include/retroforge/models/ConversionJob.hpp new file mode 100644 index 0000000..b0dd5fb --- /dev/null +++ b/core/include/retroforge/models/ConversionJob.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include "retroforge/models/Console.hpp" + +#include +#include + +namespace retroforge::models { + +class ConversionJob { + public: + ConversionJob(std::string caminho_origem, Console console_alvo); + + const std::string &caminho_origem() const { return caminho_origem_; }; + Console console_alvo() const { return console_alvo_; } + + void set_progresso(int progresso) { progresso_.store(progresso); }; + int progresso() { return progresso_.load(); }; + + void cancelar() { cancelado_.store(true); }; + bool is_cancelado() { return cancelado_.load(); }; + + private: + std::string caminho_origem_; + Console console_alvo_; + std::atomic progresso_{0}; + std::atomic cancelado_{false}; +}; + +} // namespace retroforge::models \ No newline at end of file diff --git a/core/include/retroforge/process/ExternalProcessHandle.hpp b/core/include/retroforge/process/ExternalProcessHandle.hpp index cf6c383..9eb541a 100644 --- a/core/include/retroforge/process/ExternalProcessHandle.hpp +++ b/core/include/retroforge/process/ExternalProcessHandle.hpp @@ -22,4 +22,5 @@ class ExternalProcessHandle { std::unique_ptr handle_; }; + } // namespace retroforge::process diff --git a/core/src/bootstrap/CompressionEngineFactory.cpp b/core/src/bootstrap/CompressionEngineFactory.cpp new file mode 100644 index 0000000..8cfca28 --- /dev/null +++ b/core/src/bootstrap/CompressionEngineFactory.cpp @@ -0,0 +1,39 @@ +#include "retroforge/bootstrap/CompressionEngineFactory.hpp" + +#include "retroforge/compression/converters/ChdmanConverter.hpp" +#include "retroforge/compression/converters/DolphinToolConverter.hpp" +#include "retroforge/compression/converters/LibzipPacker.hpp" +#include "retroforge/compression/converters/MaxcsoConverter.hpp" + +namespace retroforge::bootstrap { + +using models::Console; + +compression::CompressionRouter CompressionEngineFactory::criar() { + compression::CompressionRouter roteador; + + auto chdman = std::make_shared(); + + roteador.register_strategy(Console::PS1, chdman); + roteador.register_strategy(Console::PS2, chdman); + roteador.register_strategy(Console::SegaCD, chdman); + roteador.register_strategy(Console::Saturn, chdman); + roteador.register_strategy(Console::Dreamcast, chdman); + + auto dolphin_tool = std::make_shared(); + + roteador.register_strategy(Console::GameCube, dolphin_tool); + + auto maxcso = std::make_shared(); + + roteador.register_strategy(Console::PSP, maxcso); + + auto libzip = std::make_shared(); + + roteador.register_strategy(Console::SNES, libzip); + roteador.register_strategy(Console::Genesis, libzip); + + return roteador; +} + +} // namespace retroforge::bootstrap \ No newline at end of file diff --git a/core/src/compression/CompressionRouter.cpp b/core/src/compression/CompressionRouter.cpp new file mode 100644 index 0000000..c1257a0 --- /dev/null +++ b/core/src/compression/CompressionRouter.cpp @@ -0,0 +1,28 @@ +#include "retroforge/compression/CompressionRouter.hpp" + +namespace retroforge::compression { + +using models::Console; + +void CompressionRouter::register_strategy(Console console, + std::shared_ptr estrategia) { + estrategias_[console] = std::move(estrategia); +} + +ResultadoRoteamento CompressionRouter::rotear(const std::shared_ptr &job) { + if (job->console_alvo() == Console::DreamcastModificado || + job->console_alvo() == Console::PSP_PBP) { + return ResultadoRoteamento::BloqueadoPorSeguranca; + } + + auto encontrado = estrategias_.find(job->console_alvo()); + if (encontrado == estrategias_.end()) { + return ResultadoRoteamento::ConsoleNaoSuportado; + } + + bool sucesso = encontrado->second->comprimir(job); + return sucesso ? ResultadoRoteamento::ConversaoIniciada + : ResultadoRoteamento::ConsoleNaoSuportado; +} + +} // namespace retroforge::compression \ No newline at end of file diff --git a/core/src/compression/converters/ChdmanConverter.cpp b/core/src/compression/converters/ChdmanConverter.cpp new file mode 100644 index 0000000..9eef04b --- /dev/null +++ b/core/src/compression/converters/ChdmanConverter.cpp @@ -0,0 +1,40 @@ +#include "retroforge/compression/converters/ChdmanConverter.hpp" + +#include "retroforge/process/ExternalProcessHandle.hpp" + +#include +#include +#include + +namespace retroforge::compression::converters { + +namespace fs = std::filesystem; + +bool ChdmanConverter::comprimir(const std::shared_ptr &job) { + fs::path origem(job->caminho_origem()); + fs::path destino = origem; + destino.replace_extension(".chd"); + + std::string comando = + "chdman createcd -i \"" + origem.string() + "\" -o \"" + destino.string() + "\""; + + try { + process::ExternalProcessHandle processo(comando); + + std::string linha; + while (processo.read_line(linha)) { + if (job->is_cancelado()) { + return false; + } + } + + job->set_progresso(100); + return true; + + } catch (const std::exception &erro) { + std::cerr << "Falha ao converter via chdman: " << erro.what() << "\n"; + return false; + } +} + +} // namespace retroforge::compression::converters \ No newline at end of file diff --git a/core/src/compression/converters/DolphinToolConverter.cpp b/core/src/compression/converters/DolphinToolConverter.cpp new file mode 100644 index 0000000..6e4eda4 --- /dev/null +++ b/core/src/compression/converters/DolphinToolConverter.cpp @@ -0,0 +1,40 @@ +#include "retroforge/compression/converters/DolphinToolConverter.hpp" +#include "retroforge/process/ExternalProcessHandle.hpp" + +#include +#include +#include + +namespace retroforge::compression::converters { + +namespace fs = std::filesystem; + +bool DolphinToolConverter::comprimir(const std::shared_ptr &job) { + fs::path origem(job->caminho_origem()); + fs::path destino = origem; + destino.replace_extension(".rvz"); + + std::string comando = + "flatpak run --command=dolphin-tool org.DolphinEmu.dolphin-emu convert -i \"" + + origem.string() + "\" -o \"" + destino.string() + "\" -f rvz -b 131072 -c zstd -l 3"; + + try { + process::ExternalProcessHandle processo(comando); + + std::string linha; + while (processo.read_line(linha)) { + if (job->is_cancelado()) { + return false; + } + } + + job->set_progresso(100); + return true; + + } catch (const std::exception &erro) { + std::cerr << "Falha ao converter via dolphin-tool: " << erro.what() << "\n"; + return false; + } +} + +} // namespace retroforge::compression::converters \ No newline at end of file diff --git a/core/src/compression/converters/LibzipPacker.cpp b/core/src/compression/converters/LibzipPacker.cpp new file mode 100644 index 0000000..f4e9a1b --- /dev/null +++ b/core/src/compression/converters/LibzipPacker.cpp @@ -0,0 +1,78 @@ +// Arquivo : core / src / compression / converters / LibzipPacker . cpp +#include "retroforge/compression/converters/LibzipPacker.hpp" + +#include +#include +#include + +#include + +namespace retroforge::compression::converters { + +namespace fs = std::filesystem; + +namespace { + +struct ZipDiscardDeleter { + void operator()(zip_t *z) const { + if (z) { + zip_discard(z); + } + } +}; +using ScopedZipHandle = std::unique_ptr; + +} // namespace + +bool LibzipPacker::comprimir(const std::shared_ptr &job) { + fs::path origem(job->caminho_origem()); + fs::path destino = origem; + + destino.replace_extension(".zip"); + + int codigo_erro = 0; + +#if defined(_WIN32) + std::string str_destino = reinterpret_cast(destino.u8string().c_str()); + std::string str_origem = reinterpret_cast(origem.u8string().c_str()); + std::string str_nome_arquivo = + reinterpret_cast(origem.filename().u8string().c_str()); +#else + std::string str_destino = destino.string(); + std::string str_origem = origem.string(); + std::string str_nome_arquivo = origem.filename().string(); +#endif + + ScopedZipHandle zip_guard( + zip_open(str_destino.c_str(), ZIP_CREATE | ZIP_TRUNCATE, &codigo_erro)); + + if (!zip_guard) { + std::cerr << "Falha ao criar .zip (codigo libzip: " << codigo_erro << ")\n"; + return false; + } + + auto *fonte = zip_source_file(zip_guard.get(), str_origem.c_str(), 0, 0); + + if (!fonte) { + std::cerr << "Falha ao ler arquivo de origem para empacotamento.\n"; + return false; + } + + if (zip_file_add(zip_guard.get(), str_nome_arquivo.c_str(), fonte, ZIP_FL_OVERWRITE) < 0) { + zip_source_free(fonte); + std::cerr << "Falha ao adicionar arquivo dentro do .zip.\n"; + return false; + } + + zip_t *zip_raw = zip_guard.release(); + + if (zip_close(zip_raw) < 0) { + std::cerr << "Falha ao gravar arquivo .zip no disco.\n"; + return false; + } + + job->set_progresso(100); + return true; +} + +} // namespace retroforge::compression::converters \ No newline at end of file diff --git a/core/src/compression/converters/MaxcsoConverter.cpp b/core/src/compression/converters/MaxcsoConverter.cpp new file mode 100644 index 0000000..9f5da4b --- /dev/null +++ b/core/src/compression/converters/MaxcsoConverter.cpp @@ -0,0 +1,39 @@ +#include "retroforge/compression/converters/MaxcsoConverter.hpp" + +#include "retroforge/process/ExternalProcessHandle.hpp" + +#include +#include +#include + +namespace retroforge::compression::converters { + +namespace fs = std::filesystem; + +bool MaxcsoConverter::comprimir(const std::shared_ptr &job) { + fs::path origem(job->caminho_origem()); + fs::path destino = origem; + destino.replace_extension(".cso"); + + std::string comando = "maxcso \"" + origem.string() + "\" -o \"" + destino.string() + "\""; + + try { + process::ExternalProcessHandle processo(comando); + + std::string linha; + while (processo.read_line(linha)) { + if (job->is_cancelado()) { + return false; + } + } + + job->set_progresso(100); + return true; + + } catch (const std::exception &erro) { + std::cerr << "Falha ao converter via maxcso-: " << erro.what() << "\n"; + return false; + } +} + +} // namespace retroforge::compression::converters \ No newline at end of file diff --git a/core/src/models/ConversionJob.cpp b/core/src/models/ConversionJob.cpp new file mode 100644 index 0000000..5cab1e0 --- /dev/null +++ b/core/src/models/ConversionJob.cpp @@ -0,0 +1,8 @@ +#include "retroforge/models/ConversionJob.hpp" + +namespace retroforge::models { + +ConversionJob::ConversionJob(std::string caminho_origem, Console console_alvo) + : caminho_origem_(std::move(caminho_origem)), console_alvo_(console_alvo) {} + +} // namespace retroforge::models \ No newline at end of file diff --git a/docs/RetroForge_Base_Dark.tex b/docs/RetroForge_Base_Dark.tex index 5483dab..717a7db 100644 --- a/docs/RetroForge_Base_Dark.tex +++ b/docs/RetroForge_Base_Dark.tex @@ -59,9 +59,6 @@ showstringspaces=false, tabsize=4, language=C++, - numbers=left, - numberstyle=\tiny\color{gray}, - numbersep=8pt, } \newtcolorbox{CaixaInfo}[1][]{ @@ -193,5 +190,6 @@ \include{RetroForge_Modulo2} \include{RetroForge_Modulo3} \include{RetroForge_Modulo4} +\include{RetroForge_Modulo5} \end{document} \ No newline at end of file diff --git a/docs/RetroForge_Modulo3.tex b/docs/RetroForge_Modulo3.tex index a962897..cac6820 100644 --- a/docs/RetroForge_Modulo3.tex +++ b/docs/RetroForge_Modulo3.tex @@ -8,17 +8,20 @@ \section{Módulo 3: O Motor de Recomendação (Smart Context) e Compressores Ext Esse ``alguém'' é o Motor de Recomendação -- o \textbf{Smart Context} descrito na Seção 3.2 da documentação v2.0 -- e é o assunto deste módulo. \begin{CaixaInfo}[title={O que já existe vs. o que este módulo constrói}] -Do Módulo 0 e do Módulo 2, já temos prontos: \texttt{models::Console} (a única fonte de verdade do vocabulário de consoles, reexportada por \texttt{identification::Console} desde o Módulo 0 -- incluindo \texttt{DreamcastModificado}, as adições \texttt{SegaCD} e \texttt{Saturn}, \textbf{e} a distinção \texttt{PSP} vs.\ \texttt{PSP\_PBP}, todas ampliadas no Módulo 2), \texttt{ConsoleIdentifier}, os detectores de \texttt{PS1/PS2/GameCube/PSP/PBP/Dreamcast/SegaCD/Saturn}, o \texttt{CueSheetResolver}, e duas classes de infraestrutura que vamos \textbf{reaproveitar} agora pela primeira vez fora do Módulo 0: \texttt{io::BinaryFileReader} e \texttt{process::ExternalProcessHandle}. +Do Módulo 0 e do Módulo 2, já temos prontos: \texttt{models::Console} (a única fonte de verdade do vocabulário de consoles, reexportada por \texttt{identification::Console} desde o Módulo 0 -- incluindo \texttt{DreamcastModificado}, \texttt{SegaCD}, \texttt{Saturn}, a distinção \texttt{PSP} vs.\ \texttt{PSP\_PBP}, \textbf{e} os dois consoles de cartucho já com detector concreto pronto: \texttt{SNES} (via \texttt{SnesDetector}, um \texttt{IContainerDetector} de extensão \texttt{.smc}/\texttt{.sfc}) e \texttt{Genesis} (via \texttt{GenesisDetector}, um \texttt{IConsoleDetector} de assinatura binária)), \texttt{ConsoleIdentifier}, os detectores de \texttt{PS1/PS2/GameCube/PSP/PBP/Dreamcast/SegaCD/Saturn/SNES/Genesis}, o \texttt{CueSheetResolver}, a \texttt{IdentificationEngineFactory} (Módulo 2), e duas classes de infraestrutura que vamos \textbf{reaproveitar} agora pela primeira vez fora do Módulo 0: \texttt{io::BinaryFileReader} e \texttt{process::ExternalProcessHandle}. -No repositório público do projeto (\texttt{github.com/Dev-Sams1012/RetroForge}), o estado atual ainda reflete exatamente isso: as pastas \texttt{core/}, \texttt{cli/} e \texttt{external/} existem, mas \texttt{external/} só contém o submódulo \texttt{CLI11} -- não há, ainda, nenhuma pasta \texttt{compression/} dentro de \texttt{core/include/retroforge/}, nem qualquer integração real com \texttt{chdman}, \texttt{maxcso}, \texttt{dolphin-tool} ou \texttt{libzip}. +Este módulo é exatamente o que falta escrever e commitar para fechar esse gap: vamos (1) criar a interface \texttt{ICompressionStrategy}, (2) implementar \texttt{ChdmanConverter}, \texttt{DolphinToolConverter} e \texttt{MaxcsoConverter} -- todos por trás de \texttt{ExternalProcessHandle}, (3) implementar \texttt{LibzipPacker}, que \textbf{não} usa subprocesso, (4) construir o \texttt{CompressionRouter} que orquestra tudo isso e bloqueia explicitamente \textbf{os dois} valores sentinela que o Módulo 2 introduziu, e (5) -- diferente da primeira versão deste módulo -- montar essa composição inteira através de uma classe própria, \texttt{CompressionEngineFactory}, em vez de uma função solta. -Este módulo é exatamente o que falta escrever e commitar para que esse gap feche: vamos (1) criar a interface \texttt{ICompressionStrategy}, (2) implementar \texttt{ChdmanConverter}, \texttt{DolphinToolConverter} e (como exercício guiado) \texttt{MaxcsoConverter} -- todos por trás de \texttt{ExternalProcessHandle}, (3) implementar \texttt{LibzipPacker}, que \textbf{não} usa subprocesso, e (4) construir o \texttt{CompressionRouter} que orquestra tudo isso e bloqueia explicitamente \textbf{os dois} valores sentinela que o Módulo 2 introduziu. +\begin{CaixaAlerta}[title={Duas correções em relação a uma versão anterior deste módulo}] +Uma versão anterior deste módulo tinha dois problemas que esta revisão corrige: -\begin{CaixaAlerta}[title={Correção em relação a uma versão anterior deste módulo}] -Uma versão anterior deste módulo tratava apenas de \texttt{Console::DreamcastModificado} como caso a bloquear, porque, na época em que foi escrita, o Módulo 2 ainda não havia introduzido \texttt{Console::PSP\_PBP}. Isso mudou: o Módulo 2, ao formalizar o detector \texttt{PbpDetector} (Seção sobre o ``irmão perigoso do PSP''), deixou explícito que um contêiner \texttt{EBOOT.PBP} é estruturalmente o mesmo tipo de risco que um \texttt{.cdi} de Dreamcast -- e que \texttt{PSP\_PBP} ``vai precisar exatamente do mesmo tratamento de trava explícita que \texttt{DreamcastModificado} recebeu'' assim que este módulo fosse revisado. Esta é essa revisão: o \texttt{CompressionRouter} apresentado a seguir bloqueia \textbf{ambos} os valores sentinela, com o mesmo \texttt{if} explícito e antecipado -- nenhuma rota é registrada para nenhum dos dois, e a ausência de registro nunca é, sozinha, o mecanismo de segurança. +\begin{enumerate} + \item \textbf{Bloqueio incompleto:} tratava apenas de \texttt{Console::DreamcastModificado} como caso a bloquear, porque, na época em que foi escrita, o Módulo 2 ainda não havia introduzido \texttt{Console::PSP\_PBP}. O \texttt{CompressionRouter} apresentado a seguir bloqueia \textbf{ambos} os valores sentinela, com o mesmo \texttt{if} explícito e antecipado -- nenhuma rota é registrada para nenhum dos dois, e a ausência de registro nunca é, sozinha, o mecanismo de segurança. + \item \textbf{Montagem como função solta:} a composição do motor de recomendação vivia em uma função livre, \texttt{construir\_motor\_de\_recomendacao()}, escrita fora de qualquer classe -- exatamente o padrão que o Módulo 2 já havia rejeitado para o Motor de Identificação (Seção sobre \texttt{IdentificationEngineFactory}, que inclusive já previa esta correção: ``o Módulo 3 vai introduzir, no mesmo espírito, uma \texttt{CompressionEngineFactory}''). Esta revisão substitui a função solta por essa classe, na mesma pasta \texttt{bootstrap/} que já hospeda a \texttt{IdentificationEngineFactory}. +\end{enumerate} \end{CaixaAlerta} -Fora essa correção, não precisamos corrigir nenhuma outra divergência de vocabulário: como o Módulo 0 já unificou \texttt{Console} em um único arquivo (\texttt{models/Console.hpp}), \texttt{ConversionJob} e \texttt{ConsoleIdentifier} sempre enxergaram exatamente o mesmo tipo -- inclusive todos os valores introduzidos no Módulo 2. +Fora essas duas correções, não precisamos ajustar nenhuma outra divergência de vocabulário: como o Módulo 0 já unificou \texttt{Console} em um único arquivo (\texttt{models/Console.hpp}), \texttt{ConversionJob} e \texttt{ConsoleIdentifier} sempre enxergaram exatamente o mesmo tipo -- inclusive todos os valores introduzidos no Módulo 2. \end{CaixaInfo} \subsection{Do ``identificar'' ao ``agir'': por que o roteamento é uma classe separada} @@ -66,11 +69,11 @@ \subsection{Duas naturezas de compressão, uma única abstração} // Arquivo: core/include/retroforge/compression/ICompressionStrategy.hpp #pragma once +#include "retroforge/models/ConversionJob.hpp" + #include #include -#include "retroforge/models/ConversionJob.hpp" - namespace retroforge::compression { // Responsabilidade unica: dado um ConversionJob ja identificado, produzir @@ -82,8 +85,6 @@ \subsection{Duas naturezas de compressão, uma única abstração} virtual ~ICompressionStrategy() = default; // Executa a compressao de fato. Retorna true em caso de sucesso. - // job->set_progresso(...) pode ser chamado durante a execucao para - // alimentar a barra de progresso da interface (CLI ou GUI). virtual bool comprimir(const std::shared_ptr& job) = 0; // Extensao do arquivo de SAIDA que esta estrategia produz (".chd", @@ -95,27 +96,23 @@ \subsection{Duas naturezas de compressão, uma única abstração} } // namespace retroforge::compression \end{CaixaCodigo} -\begin{CaixaInfo} -Repare que \texttt{ICompressionStrategy} depende de \texttt{models::ConversionJob}, e não do contrário -- \texttt{ConversionJob} continua sem saber nada sobre compressão, exatamente como o Módulo 0 estabeleceu (\textit{``ela não sabe \textbf{como} converter''}). -A seta de dependência aponta sempre de quem sabe menos para quem sabe mais sobre o domínio específico: \texttt{compression/} depende de \texttt{models/}, nunca o inverso. -\end{CaixaInfo} +\begin{CaixaAlerta} +Repare que \texttt{} é incluído explicitamente aqui, mesmo que \texttt{models/ConversionJob.hpp} já traga o cabeçalho transitivamente. Depender de uma inclusão transitiva para compilar é uma armadilha comum -- funciona hoje, mas quebra silenciosamente no dia em que alguém reorganizar os includes de \texttt{ConversionJob.hpp}. Cada arquivo deve incluir exatamente o que usa diretamente, nunca menos. +\end{CaixaAlerta} \begin{CaixaInfo} -Vale uma pausa para apreciar um não-evento: diferente do que aconteceria se o projeto tivesse dois enums \texttt{Console} independentes (um em \texttt{models/}, outro em \texttt{identification/}), \texttt{job->console\_alvo()} já devolve, neste exato momento, o mesmo tipo -- incluindo \texttt{DreamcastModificado}, \texttt{PSP\_PBP}, \texttt{SegaCD} e \texttt{Saturn} -- que \texttt{ConsoleIdentifier::identify()} produziu no Módulo 2. Isso só é verdade porque o Módulo 0 tomou a decisão de extrair \texttt{Console} para \texttt{models/Console.hpp} \textbf{antes} de qualquer código depender dele, e o Módulo 2 amplificou aquele único arquivo, não uma cópia. +Vale uma pausa para apreciar um não-evento: diferente do que aconteceria se o projeto tivesse dois enums \texttt{Console} independentes (um em \texttt{models/}, outro em \texttt{identification/}), \texttt{job->console\_alvo()} já devolve, neste exato momento, o mesmo tipo -- incluindo \texttt{DreamcastModificado}, \texttt{PSP\_PBP}, \texttt{SegaCD}, \texttt{Saturn}, \texttt{SNES} e \texttt{Genesis} -- que \texttt{ConsoleIdentifier::identify()} produziu no Módulo 2. Isso só é verdade porque o Módulo 0 tomou a decisão de extrair \texttt{Console} para \texttt{models/Console.hpp} \textbf{antes} de qualquer código depender dele, e o Módulo 2 amplificou aquele único arquivo, não uma cópia. O \texttt{CompressionRouter} que vamos construir nesta seção pode, portanto, confiar cegamente em \texttt{job->console\_alvo()} para distinguir rotas específicas, sem nenhuma etapa de conciliação entre vocabulários. \end{CaixaInfo} -\subsection{As estratégias baseadas em subprocesso: \texttt{ChdmanConverter} e \texttt{DolphinToolConverter}} +\subsection{As estratégias baseadas em subprocesso: \texttt{ChdmanConverter}, \texttt{DolphinToolConverter} e \texttt{MaxcsoConverter}} -Com o vocabulário já unificado desde o Módulo 0 e expandido no Módulo 2, podemos construir as estratégias de verdade. -As três ferramentas externas de disco óptico/Nintendo compartilham o mesmo mecanismo -- abrir um processo filho via \texttt{ExternalProcessHandle} (Módulo 0) e ler sua saída -- mudando apenas o comando montado e a extensão de saída. +As três ferramentas externas de disco óptico/Nintendo/PSP compartilham o mesmo mecanismo -- abrir um processo filho via \texttt{ExternalProcessHandle} (Módulo 0) e ler sua saída -- mudando apenas o comando montado e a extensão de saída. \begin{itemize} \item \textbf{Por que \texttt{ChdmanConverter} existe:} converte imagens de disco óptico (PS1, PS2, Sega CD, Saturn e Dreamcast limpo) para \texttt{.chd}, delegando o trabalho pesado ao executável \texttt{chdman} via subprocesso. -Sua única responsabilidade é montar o comando correto e supervisionar a execução -- ela não decide \textit{quando} deve ser chamada (isso é papel do \texttt{CompressionRouter}). - \item \textbf{Como se comunica com outras classes:} usa \texttt{process::ExternalProcessHandle} como dependência interna (\texttt{unique\_ptr}, posse exclusiva -- ninguém mais precisa desse handle); -recebe um \texttt{std::shared\_ptr} do \texttt{CompressionRouter} e chama \texttt{set\_progresso} nele conforme lê a saída do processo. - \item \textbf{Onde ela mora:} \texttt{core/include/retroforge/compression/converters/ChdmanConverter.hpp} e \texttt{core/src/compression/converters/ChdmanConverter.cpp} -- uma subpasta \texttt{converters/} dentro de \texttt{compression/}, no mesmo espírito da subpasta \texttt{detectors/} dentro de \texttt{identification/} (Módulo 0). + \item \textbf{Como se comunica com outras classes:} usa \texttt{process::ExternalProcessHandle} como dependência interna (\texttt{unique\_ptr}, posse exclusiva); recebe um \texttt{std::shared\_ptr} do \texttt{CompressionRouter} e chama \texttt{set\_progresso} nele conforme lê a saída do processo. + \item \textbf{Onde ela mora:} \texttt{core/include/retroforge/compression/converters/ChdmanConverter.hpp} e \texttt{core/src/compression/converters/ChdmanConverter.cpp}. \end{itemize} \begin{CaixaCodigo} @@ -126,9 +123,6 @@ \subsection{As estratégias baseadas em subprocesso: \texttt{ChdmanConverter} e namespace retroforge::compression::converters { -// Converte discos opticos (PS1, PS2, Sega CD, Saturn, Dreamcast .gdi) para .chd via -// subprocesso chdman. Reaproveita process::ExternalProcessHandle do -// Modulo 0 -- este modulo NAO reinventa gerenciamento de popen/pclose. class ChdmanConverter : public ICompressionStrategy { public: bool comprimir(const std::shared_ptr& job) override; @@ -142,11 +136,12 @@ \subsection{As estratégias baseadas em subprocesso: \texttt{ChdmanConverter} e // Arquivo: core/src/compression/converters/ChdmanConverter.cpp #include "retroforge/compression/converters/ChdmanConverter.hpp" +#include "retroforge/process/ExternalProcessHandle.hpp" + +#include #include #include -#include "retroforge/process/ExternalProcessHandle.hpp" - namespace retroforge::compression::converters { namespace fs = std::filesystem; @@ -156,11 +151,6 @@ \subsection{As estratégias baseadas em subprocesso: \texttt{ChdmanConverter} e fs::path destino = origem; destino.replace_extension(".chd"); - // NOTA: a construcao do comando aqui e' deliberadamente simples. - // Escapar caminhos com espacos/aspas de forma robusta, capturar - // stderr separadamente e interpretar codigos de saida numericos sao - // temas do Modulo 8 (Gerenciamento Avancado de Subprocessos) -- este - // modulo foca em ROTEAMENTO, nao em blindagem de IPC. std::string comando = "chdman createcd -i \"" + origem.string() + "\" -o \"" + destino.string() + "\""; @@ -169,18 +159,14 @@ \subsection{As estratégias baseadas em subprocesso: \texttt{ChdmanConverter} e std::string linha; while (processo.read_line(linha)) { - if (job->foi_cancelado()) { - // O encerramento efetivo do processo filho em cancelamentos - // (SIGTERM isolado, US05) sera formalizado no Modulo 4, - // quando o Thread Pool introduzir o controle granular. + if (job->is_cancelado()) { return false; } - // Leitura linha a linha permite, no futuro (Modulo 9), calcular - // um percentual real a partir da saida textual do chdman. } job->set_progresso(100); return true; + } catch (const std::exception& erro) { std::cerr << "Falha ao converter via chdman: " << erro.what() << "\n"; return false; @@ -190,15 +176,7 @@ \subsection{As estratégias baseadas em subprocesso: \texttt{ChdmanConverter} e } // namespace retroforge::compression::converters \end{CaixaCodigo} -\begin{CaixaAlerta} -Note o \texttt{try/catch} envolvendo a criação do \texttt{ExternalProcessHandle}. -Isso é intencional e antecipa o Módulo 9 (Tratamento de Erros): se o \texttt{chdman} não estiver instalado, ou o \texttt{popen} falhar por qualquer motivo, o construtor de \texttt{ExternalProcessHandle} lança \texttt{std::runtime\_error} (Módulo 0). -Sem este \texttt{try/catch}, uma única falha de subprocesso derrubaria \textbf{todo o programa} -- inaceitável para uma ferramenta que processa filas de 50+ jogos (US03/US08). -O Módulo 9 vai generalizar esse padrão para toda a Thread Pool; -aqui, aplicamos a versão mais simples da mesma ideia. -\end{CaixaAlerta} - -\texttt{DolphinToolConverter} segue o mesmo molde, mas com uma diferença de negócio importante, já prevista na documentação v2.0 (Seção 3.2): a rota Nintendo avisa o usuário sobre ``a eliminação inteligente de dados preenchidos (\textit{junk data})'' -- uma flag específica do \texttt{dolphin-tool}, sem equivalente no \texttt{chdman}. +\texttt{DolphinToolConverter} segue o mesmo molde, com a rota Nintendo habilitando a eliminação de ``junk data'' (Seção 3.2 da documentação v2.0): \begin{CaixaCodigo} // Arquivo: core/include/retroforge/compression/converters/DolphinToolConverter.hpp @@ -208,9 +186,6 @@ \subsection{As estratégias baseadas em subprocesso: \texttt{ChdmanConverter} e namespace retroforge::compression::converters { -// Converte ISOs de GameCube/Wii para .rvz via subprocesso dolphin-tool. -// A eliminacao de "junk data" (Secao 3.2 da documentacao v2.0) e' habilitada -// por padrao -- e' o que torna o RVZ tao mais compacto que um .iso puro. class DolphinToolConverter : public ICompressionStrategy { public: bool comprimir(const std::shared_ptr& job) override; @@ -224,11 +199,12 @@ \subsection{As estratégias baseadas em subprocesso: \texttt{ChdmanConverter} e // Arquivo: core/src/compression/converters/DolphinToolConverter.cpp #include "retroforge/compression/converters/DolphinToolConverter.hpp" +#include "retroforge/process/ExternalProcessHandle.hpp" + +#include #include #include -#include "retroforge/process/ExternalProcessHandle.hpp" - namespace retroforge::compression::converters { namespace fs = std::filesystem; @@ -238,23 +214,22 @@ \subsection{As estratégias baseadas em subprocesso: \texttt{ChdmanConverter} e fs::path destino = origem; destino.replace_extension(".rvz"); - // "-b 131072 -c zstd -l 5" e' um preset conservador de compressao RVZ; - // sera parametrizavel pela interface (CLI/GUI) a partir do Modulo 11. std::string comando = "dolphin-tool convert -i \"" + origem.string() + "\" -o \"" + - destino.string() + "\" -f rvz -b 131072 -c zstd -l 5"; + destino.string() + "\" -f rvz -b 131072 -c zstd -l 5"; try { process::ExternalProcessHandle processo(comando); std::string linha; while (processo.read_line(linha)) { - if (job->foi_cancelado()) { + if (job->is_cancelado()) { return false; } } job->set_progresso(100); return true; + } catch (const std::exception& erro) { std::cerr << "Falha ao converter via dolphin-tool: " << erro.what() << "\n"; return false; @@ -264,22 +239,77 @@ \subsection{As estratégias baseadas em subprocesso: \texttt{ChdmanConverter} e } // namespace retroforge::compression::converters \end{CaixaCodigo} -\begin{CaixaInfo} -\texttt{MaxcsoConverter} (para \texttt{Console::PSP} -- o \texttt{.iso} cru, \textbf{nunca} \texttt{PSP\_PBP} -- $\rightarrow$ \texttt{.cso}) é estruturalmente idêntico aos dois acima -- só o comando (\texttt{maxcso "origem" -o "destino"}) e a extensão (\texttt{.cso}) mudam. -Por isso, assim como o Módulo 2 fez com \texttt{PspDetector}, deixamos sua implementação completa como o Exercício 3.1: o valor didático está em você reconhecer o padrão repetido, não em copiar um quarto bloco de código quase idêntico. -\end{CaixaInfo} +\texttt{MaxcsoConverter} (para \texttt{Console::PSP} -- o \texttt{.iso} cru, \textbf{nunca} \texttt{PSP\_PBP} -- $\rightarrow$ \texttt{.cso}) segue exatamente o mesmo molde: + +\begin{CaixaCodigo} +// Arquivo: core/include/retroforge/compression/converters/MaxcsoConverter.hpp +#pragma once + +#include "retroforge/compression/ICompressionStrategy.hpp" + +namespace retroforge::compression::converters { + +class MaxcsoConverter : public ICompressionStrategy { +public: + bool comprimir(const std::shared_ptr& job) override; + std::string extensao_saida() const override { return ".cso"; } +}; + +} // namespace retroforge::compression::converters +\end{CaixaCodigo} + +\begin{CaixaCodigo} +// Arquivo: core/src/compression/converters/MaxcsoConverter.cpp +#include "retroforge/compression/converters/MaxcsoConverter.hpp" + +#include "retroforge/process/ExternalProcessHandle.hpp" + +#include +#include +#include + +namespace retroforge::compression::converters { + +namespace fs = std::filesystem; + +bool MaxcsoConverter::comprimir(const std::shared_ptr& job) { + fs::path origem(job->caminho_origem()); + fs::path destino = origem; + destino.replace_extension(".cso"); + + std::string comando = "maxcso \"" + origem.string() + "\" -o \"" + destino.string() + "\""; + + try { + process::ExternalProcessHandle processo(comando); + + std::string linha; + while (processo.read_line(linha)) { + if (job->is_cancelado()) { + return false; + } + } + + job->set_progresso(100); + return true; + + } catch (const std::exception& erro) { + // Mensagem de erro corrigida: esta funcao usa maxcso, nao chdman. + std::cerr << "Falha ao converter via maxcso: " << erro.what() << "\n"; + return false; + } +} + +} // namespace retroforge::compression::converters +\end{CaixaCodigo} \subsection{A estratégia em processo: \texttt{LibzipPacker}} -Cartuchos e coleções de Arcade (\texttt{.sfc}, \texttt{.md}, entre outros -- Seção 3.2 da documentação v2.0) não passam por nenhum executável externo: o próprio C++ do RetroForge encapsula o arquivo em \texttt{.zip} usando a biblioteca \texttt{libzip}, estaticamente linkada. -Isso muda o mecanismo interno por completo, mas -- como já estabelecido -- \textbf{não} muda o contrato que \texttt{ICompressionStrategy} exige. +Cartuchos (\texttt{Console::SNES}, \texttt{Console::Genesis} -- Seção 3.2 da documentação v2.0) não passam por nenhum executável externo: o próprio C++ do RetroForge encapsula o arquivo em \texttt{.zip} usando a biblioteca \texttt{libzip}, estaticamente linkada. \begin{itemize} - \item \textbf{Por que \texttt{LibzipPacker} existe:} sua única responsabilidade é envolver um arquivo de cartucho/arcade em um contêiner \texttt{.zip} válido, usando chamadas diretas da API C do \texttt{libzip} -- sem subprocesso, sem \texttt{popen}, sem depender de nenhum executável externo estar instalado no sistema. + \item \textbf{Por que \texttt{LibzipPacker} existe:} sua única responsabilidade é envolver um arquivo de cartucho em um contêiner \texttt{.zip} válido, usando chamadas diretas da API C do \texttt{libzip} -- sem subprocesso, sem \texttt{popen}, sem depender de nenhum executável externo estar instalado no sistema. \item \textbf{Como se comunica com outras classes:} implementa \texttt{ICompressionStrategy}, exatamente como as três classes anteriores -- para o \texttt{CompressionRouter}, ela é indistinguível das demais. -Internamente, usa uma classe auxiliar \texttt{LibzipHandle} (RAII, Módulo 0) para garantir que \texttt{zip\_close} seja sempre chamado. - \item \textbf{Onde ela mora:} \texttt{core/include/retroforge/compression/converters/LibzipPacker.hpp} e seu \texttt{.cpp} correspondente -- a mesma subpasta \texttt{converters/} das estratégias de subprocesso, porque do ponto de vista arquitetural ela \textbf{é} uma estratégia de compressão como as outras; -a subpasta é organizada por \textit{responsabilidade de negócio} (``estratégias de compressão''), não por mecanismo técnico (subprocesso vs.\ biblioteca) -- o mesmo raciocínio que o Módulo 0 usou para justificar \texttt{io/}, \texttt{process/} e \texttt{identification/} como divisões por área, não por tipo de dado. + \item \textbf{Onde ela mora:} \texttt{core/include/retroforge/compression/converters/LibzipPacker.hpp} e \texttt{core/src/compression/converters/LibzipPacker.cpp}. \end{itemize} \begin{CaixaCodigo} @@ -290,10 +320,6 @@ \subsection{A estratégia em processo: \texttt{LibzipPacker}} namespace retroforge::compression::converters { -// Empacota cartuchos/arcade (.sfc, .md, colecoes) em .zip usando libzip, -// estaticamente linkada. Diferente de ChdmanConverter/MaxcsoConverter/ -// DolphinToolConverter, NAO abre um processo filho -- a chamada acontece -// dentro do proprio processo do RetroForge (RAII via LibzipHandle interno). class LibzipPacker : public ICompressionStrategy { public: bool comprimir(const std::shared_ptr& job) override; @@ -303,6 +329,47 @@ \subsection{A estratégia em processo: \texttt{LibzipPacker}} } // namespace retroforge::compression::converters \end{CaixaCodigo} +\begin{CaixaInfo}[title={Compilando \texttt{libzip} em Windows e Linux}] +O texto original deste módulo deixava a integração com \texttt{libzip} em aberto (\textit{``decisão de build ainda pendente''}). Na prática, a forma mais robusta de resolver isso \textbf{sem} depender de um pacote instalado manualmente pelo usuário em cada sistema operacional é ramificar o \texttt{core/CMakeLists.txt} por plataforma: no Linux, \texttt{pkg-config} localiza o \texttt{libzip-dev} do sistema (instalado via \texttt{apt} no CI, no mesmo espírito do \texttt{clang-format}); no Windows (e macOS), \texttt{find\_package(libzip CONFIG REQUIRED)} espera um pacote gerenciado (tipicamente via \texttt{vcpkg}), que já expõe um alvo \texttt{CMake} nativo. Em ambos os casos, o projeto termina com um único alvo lógico, \texttt{libzip::zip}, que o restante do \texttt{CMakeLists.txt} linka sem precisar saber qual caminho foi usado para resolvê-lo: + +\begin{CaixaCodigo} +# Arquivo: core/CMakeLists.txt (trecho adicionado no topo) +if(UNIX AND NOT APPLE) + find_package(PkgConfig REQUIRED) + pkg_check_modules(LIBZIP REQUIRED IMPORTED_TARGET libzip) + + if(TARGET PkgConfig::LIBZIP AND NOT TARGET libzip::zip) + add_library(libzip::zip ALIAS PkgConfig::LIBZIP) + endif() +else() + # Windows e macOS usam o find_package nativo / vcpkg + find_package(libzip CONFIG REQUIRED) +endif() + +# ... (targets existentes) ... + +target_link_libraries(retroforge_core PRIVATE + libzip::zip +) +\end{CaixaCodigo} + +No \texttt{.github/workflows/cmake-single-platform.yml}, o passo de instalação de dependências do sistema precisa crescer para incluir \texttt{pkg-config} e \texttt{libzip-dev} (Ubuntu, runner atual do CI), antes mesmo da checagem de formatação: + +\begin{CaixaCodigo} +# Arquivo: .github/workflows/cmake-single-platform.yml (trecho revisado) +- name: Install System Dependencies + run: | + sudo apt-get update + sudo apt-get install -y clang-format pkg-config libzip-dev + +- name: Check Code Formatting + run: | + find core cli -type f \( -name "*.cpp" -o -name "*.hpp" -o -name "*.h" \) | xargs -r clang-format --dry-run --Werror +\end{CaixaCodigo} + +Isso antecipa, de forma concreta, o multiplataforma que o Módulo 13 (CI/CD Avançado) só formaliza mais tarde -- mas resolver a dependência de \texttt{libzip} sem travar o Windows já é necessário agora, e não faz sentido adiar. +\end{CaixaInfo} + \begin{CaixaCodigo} // Arquivo: core/src/compression/converters/LibzipPacker.cpp #include "retroforge/compression/converters/LibzipPacker.hpp" @@ -311,7 +378,7 @@ \subsection{A estratégia em processo: \texttt{LibzipPacker}} #include #include -#include // API C do libzip +#include namespace retroforge::compression::converters { @@ -319,17 +386,20 @@ \subsection{A estratégia em processo: \texttt{LibzipPacker}} namespace { -// RAII minimo para o handle zip_t*, no mesmo espirito de -// ExternalProcessHandle (Modulo 0): o deleter customizado garante que -// zip_close() seja chamado mesmo se uma excecao interromper o fluxo. -struct FechadorDeZip { +// zip_discard() (nao zip_close()) e' o deleter correto para o caminho de +// RAII "normal": zip_close() so' e' chamado explicitamente apos o sucesso +// de zip_file_add, e so' ENTAO o handle e' liberado do unique_ptr via +// release() -- ver comprimir() abaixo. Se qualquer 'return false' anterior +// disparar, o destrutor chama zip_discard(), que fecha o arquivo SEM +// gravar as mudancas -- nunca deixando um .zip corrompido/parcial no disco. +struct ZipDiscardDeleter { void operator()(zip_t* z) const { if (z) { - zip_close(z); + zip_discard(z); } } }; -using LibzipHandle = std::unique_ptr; +using ScopedZipHandle = std::unique_ptr; } // namespace @@ -339,62 +409,73 @@ \subsection{A estratégia em processo: \texttt{LibzipPacker}} destino.replace_extension(".zip"); int codigo_erro = 0; - LibzipHandle arquivo_zip( - zip_open(destino.string().c_str(), ZIP_CREATE | ZIP_TRUNCATE, &codigo_erro)); - if (!arquivo_zip) { +#if defined(_WIN32) + // No Windows, std::filesystem::path armazena wchar_t internamente; a + // API C do libzip espera UTF-8 em todas as plataformas -- u8string() + // faz essa conversao explicitamente, em vez de confiar em .string(), + // que no Windows usaria a codepage local e corromperia nomes com + // acentos ou caracteres fora do ASCII. + std::string str_destino = reinterpret_cast(destino.u8string().c_str()); + std::string str_origem = reinterpret_cast(origem.u8string().c_str()); + std::string str_nome_arquivo = + reinterpret_cast(origem.filename().u8string().c_str()); +#else + std::string str_destino = destino.string(); + std::string str_origem = origem.string(); + std::string str_nome_arquivo = origem.filename().string(); +#endif + + ScopedZipHandle zip_guard( + zip_open(str_destino.c_str(), ZIP_CREATE | ZIP_TRUNCATE, &codigo_erro)); + + if (!zip_guard) { std::cerr << "Falha ao criar .zip (codigo libzip: " << codigo_erro << ")\n"; return false; } - zip_source_t* fonte = zip_source_file(arquivo_zip.get(), origem.string().c_str(), 0, 0); + auto* fonte = zip_source_file(zip_guard.get(), str_origem.c_str(), 0, 0); if (!fonte) { std::cerr << "Falha ao ler arquivo de origem para empacotamento.\n"; return false; } - // zip_file_add assume posse de 'fonte' em caso de sucesso; usamos - // apenas o nome do arquivo (sem o caminho completo) dentro do .zip. - if (zip_file_add(arquivo_zip.get(), origem.filename().string().c_str(), fonte, - ZIP_FL_OVERWRITE) < 0) { + if (zip_file_add(zip_guard.get(), str_nome_arquivo.c_str(), fonte, ZIP_FL_OVERWRITE) < 0) { zip_source_free(fonte); std::cerr << "Falha ao adicionar arquivo dentro do .zip.\n"; return false; } + // release() transfere a posse manualmente para fora do unique_ptr: + // a partir daqui, SOMOS NOS quem decide como fechar o handle -- por + // zip_close() (grava de verdade), nunca mais por zip_discard(). + zip_t* zip_raw = zip_guard.release(); + + if (zip_close(zip_raw) < 0) { + std::cerr << "Falha ao gravar arquivo .zip no disco.\n"; + return false; + } + job->set_progresso(100); return true; - // zip_close() real acontece no destrutor de LibzipHandle } } // namespace retroforge::compression::converters \end{CaixaCodigo} -\begin{CaixaAlerta} -\texttt{\#include } só compila se o \texttt{libzip} estiver de fato disponível para o compilador -- e isso é uma decisão de \textbf{build} ainda pendente, não resolvida neste módulo. -O Módulo 1 já estabeleceu a regra prática (\textit{``header-only $\Rightarrow$ \texttt{target\_include\_directories}; compilada $\Rightarrow$ \texttt{find\_package} ou \texttt{FetchContent} + \texttt{target\_link\_libraries}''}). -\texttt{libzip} tem código \texttt{.c} compilado, então cairá em uma dessas duas estratégias -- provavelmente \texttt{find\_package(libzip REQUIRED)}, no mesmo estilo já usado no CI atual para instalar o \texttt{clang-format} via \texttt{apt-get}. -Formalizar essa linha em \texttt{core/CMakeLists.txt} é trabalho de infraestrutura de build, não de regra de negócio -- por isso não antecipamos aqui, mas o código desta seção já pressupõe que essa linha vai existir antes de o projeto compilar de verdade. -\end{CaixaAlerta} - \subsection{O coração do módulo: \texttt{CompressionRouter}} Chegamos à classe que dá nome ao módulo. -O \texttt{CompressionRouter} é o Smart Context da documentação: ele recebe um \texttt{ConversionJob} já identificado e decide, com base no \texttt{Console} alvo, qual \texttt{ICompressionStrategy} executar -- ou se a execução deve ser \textbf{bloqueada}. +O \texttt{CompressionRouter} é o Smart Context da documentação: ele recebe um \texttt{ConversionJob} já identificado e decide, com base no \texttt{Console} alvo, qual \texttt{ICompressionStrategy} executar -- ou se a execução deve ser \textbf{bloqueada}. A classe em si não muda em relação à primeira versão deste módulo -- o que muda, na próxima seção, é \textbf{quem a monta}. \begin{itemize} - \item \textbf{Por que \texttt{CompressionRouter} existe:} é a única classe que conhece a tabela de roteamento completa (``PS1/PS2/Sega CD/Saturn/Dreamcast limpo $\rightarrow$ CHD; GameCube $\rightarrow$ RVZ; PSP (cru) $\rightarrow$ CSO; \texttt{DreamcastModificado} $\rightarrow$ bloqueado; \texttt{PSP\_PBP} $\rightarrow$ bloqueado''). + \item \textbf{Por que \texttt{CompressionRouter} existe:} é a única classe que conhece a tabela de roteamento completa. Nenhuma outra classe do projeto -- nem os conversores, nem o identificador -- precisa saber essa tabela inteira. - \item \textbf{Como se comunica com outras classes:} depende apenas da abstração \texttt{ICompressionStrategy} (nunca de \texttt{ChdmanConverter} diretamente -- Inversão de Dependência), recebe estratégias via \texttt{register\_strategy} (Princípio Aberto/Fechado, no mesmo espírito de \texttt{ConsoleIdentifier::register\_detector} do Módulo 0) e é consumido pela camada de apresentação (\texttt{cli/}, e futuramente \texttt{gui/}) depois que \texttt{ConsoleIdentifier::identify} já devolveu um \texttt{Console}. - \item \textbf{Onde ela mora:} \texttt{core/include/retroforge/compression/CompressionRouter.hpp} e \texttt{core/src/compression/CompressionRouter.cpp} -- direto dentro de \texttt{compression/}, ao lado de \texttt{ICompressionStrategy.hpp} (e não dentro de \texttt{converters/}, porque ela não é uma estratégia -- ela \textit{orquestra} estratégias, o mesmo papel que \texttt{ConsoleIdentifier} tem em relação aos detectores). + \item \textbf{Como se comunica com outras classes:} depende apenas da abstração \texttt{ICompressionStrategy} (Inversão de Dependência), recebe estratégias via \texttt{register\_strategy} (Princípio Aberto/Fechado) e é consumido pela camada de apresentação depois que \texttt{ConsoleIdentifier::identify} já devolveu um \texttt{Console}. + \item \textbf{Onde ela mora:} \texttt{core/include/retroforge/compression/CompressionRouter.hpp} e \texttt{core/src/compression/CompressionRouter.cpp} -- direto dentro de \texttt{compression/}, ao lado de \texttt{ICompressionStrategy.hpp} (e não dentro de \texttt{converters/}, porque ela \textit{orquestra} estratégias, não é uma). \end{itemize} -\begin{CaixaInfo} -Repare que \texttt{CompressionRouter.hpp} inclui \texttt{retroforge/models/Console.hpp} -- não \texttt{retroforge/identification/IConsoleDetector.hpp}. Isso não é acidente: \texttt{compression/} precisa apenas do \textbf{tipo} \texttt{Console} para montar seu mapa de roteamento, nunca da interface \texttt{IConsoleDetector} em si (isso é problema exclusivo do Módulo 2). -Se \texttt{Console} continuasse ``morando'' dentro de \texttt{IConsoleDetector.hpp}, todo arquivo que precisasse apenas do enum seria forçado a incluir a interface inteira de detecção junto -- um acoplamento acidental entre \texttt{compression/} e \texttt{identification/} que a decisão do Módulo 0 (extrair \texttt{Console} para seu próprio arquivo neutro) evita por completo. -\end{CaixaInfo} - \begin{CaixaCodigo} // Arquivo: core/include/retroforge/compression/CompressionRouter.hpp #pragma once @@ -408,10 +489,6 @@ \subsection{O coração do módulo: \texttt{CompressionRouter}} namespace retroforge::compression { -// Resultado do roteamento -- distinto de "sucesso/falha da compressao em -// si" (isso e' o bool devolvido por ICompressionStrategy::comprimir). -// Aqui distinguimos POR QUE um job nao foi comprimido: falta de rota -// registrada e' um problema diferente de bloqueio de seguranca. enum class ResultadoRoteamento { ConversaoIniciada, BloqueadoPorSeguranca, @@ -420,9 +497,6 @@ \subsection{O coração do módulo: \texttt{CompressionRouter}} class CompressionRouter { public: - // Aberto para extensao: registrar uma nova rota Console -> estrategia - // e' uma chamada a mais aqui, sem alterar rotear() (Principio - // Aberto/Fechado, identico ao ConsoleIdentifier do Modulo 0). void register_strategy(models::Console console, std::shared_ptr estrategia); @@ -438,7 +512,6 @@ \subsection{O coração do módulo: \texttt{CompressionRouter}} \begin{CaixaCodigo} // Arquivo: core/src/compression/CompressionRouter.cpp -// (CORRIGIDO neste modulo -- ver CaixaAlerta no inicio da secao para o motivo) #include "retroforge/compression/CompressionRouter.hpp" namespace retroforge::compression { @@ -451,13 +524,10 @@ \subsection{O coração do módulo: \texttt{CompressionRouter}} } ResultadoRoteamento CompressionRouter::rotear(const std::shared_ptr& job) { - // A trava de seguranca (US06 e sua extensao ao PSP, Modulo 2) vem - // ANTES de qualquer consulta ao mapa de estrategias. Dois valores de - // Console sao sentinelas de risco -- DreamcastModificado (.cdi) e - // PSP_PBP (EBOOT.PBP ja empacotado) -- e NENHUM dos dois pode ser - // alcancado, mesmo que, por engano, alguem registre uma estrategia - // para eles no futuro (ver construir_motor_de_recomendacao() adiante, - // que deliberadamente nunca registra rota para nenhum dos dois). + // A trava de seguranca vem ANTES de qualquer consulta ao mapa de + // estrategias -- DreamcastModificado (.cdi) e PSP_PBP (EBOOT.PBP ja + // empacotado) nunca sao alcancados, mesmo que, por engano, alguem + // registre uma estrategia para eles no futuro. if (job->console_alvo() == Console::DreamcastModificado || job->console_alvo() == Console::PSP_PBP) { return ResultadoRoteamento::BloqueadoPorSeguranca; @@ -469,8 +539,7 @@ \subsection{O coração do módulo: \texttt{CompressionRouter}} } bool sucesso = encontrado->second->comprimir(job); - return sucesso ? - ResultadoRoteamento::ConversaoIniciada + return sucesso ? ResultadoRoteamento::ConversaoIniciada : ResultadoRoteamento::ConsoleNaoSuportado; } @@ -478,25 +547,68 @@ \subsection{O coração do módulo: \texttt{CompressionRouter}} \end{CaixaCodigo} \begin{CaixaAlerta} -Repare que o bloqueio de \texttt{DreamcastModificado} \textbf{e} de \texttt{PSP\_PBP} é feito com um único \texttt{if} \textbf{explícito}, checado antes de qualquer coisa -- não com um simples ``esquecer de registrar uma estratégia para esses valores''. -A diferença é sutil, mas crucial: se dependêssemos apenas de \textit{não registrar} uma estratégia, um futuro contribuidor (Módulo 14) poderia, por engano, registrar \texttt{MaxcsoConverter} também para \texttt{PSP\_PBP} ``porque afinal é PSP, e PSP vai para o \texttt{maxcso}'' -- reintroduzindo exatamente o bug motivador do projeto, desta vez pela porta do PSP em vez da porta do Dreamcast. A trava explícita, verificada antes do \texttt{unordered\_map} e cobrindo \textbf{ambos} os valores sentinela, é \textbf{impossível de contornar por acidente} -- mesmo que o \texttt{enum class Console} ganhe um terceiro, quarto ou quinto valor sentinela em módulos futuros, o padrão a seguir já está estabelecido aqui. +Repare que o bloqueio de \texttt{DreamcastModificado} \textbf{e} de \texttt{PSP\_PBP} é feito com um único \texttt{if} \textbf{explícito}, checado antes de qualquer coisa -- não com um simples ``esquecer de registrar uma estratégia para esses valores''. A trava explícita, verificada antes do \texttt{unordered\_map} e cobrindo \textbf{ambos} os valores sentinela, é impossível de contornar por acidente -- mesmo que o \texttt{enum class Console} ganhe um terceiro valor sentinela em módulos futuros, o padrão a seguir já está estabelecido aqui. \end{CaixaAlerta} -Montando o motor de recomendação completo -- o equivalente, para compressão, da função \texttt{construir\_motor\_de\_identificacao} do Módulo 2: +\subsection{Montando o motor completo: \texttt{CompressionEngineFactory}} +\label{sec:compression-engine-factory} + +Assim como o Motor de Identificação (Módulo 2) parou de depender de uma função solta assim que \texttt{IdentificationEngineFactory} nasceu, o Motor de Recomendação merece exatamente o mesmo tratamento. Decidir quais estratégias existem, para quais consoles cada uma é registrada, e em que ordem isso acontece é, tanto quanto a montagem do Motor de Identificação, uma decisão de regra de negócio -- não um detalhe de demonstração que deveria viver dentro de um \texttt{main()} ou de uma função livre em algum arquivo de cabeçalho solto. + +\begin{itemize} + \item \textbf{Por que \texttt{CompressionEngineFactory} existe:} sua única responsabilidade é saber \textit{compor} um \texttt{CompressionRouter} completo -- registrar cada \texttt{ICompressionStrategy} concreta para os consoles corretos. Ela não sabe \textbf{como} nenhuma estratégia comprime -- isso continua sendo, exclusivamente, problema de cada \texttt{ICompressionStrategy} concreta. + \item \textbf{Como se comunica com outras classes:} depende de \texttt{CompressionRouter} e de todos os conversores concretos de \texttt{compression::converters} (ela é, de propósito, a única classe do projeto que precisa conhecer essa lista inteira). É consumida pela camada de apresentação (\texttt{cli/}, Módulo 11, e futuramente \texttt{gui/}) e pelo \texttt{BatchConversionManager} (Módulo 4), que passam a chamar apenas \texttt{CompressionEngineFactory::criar()} em vez de repetir a lista de \texttt{register\_strategy} em cada ponto de entrada do programa. + \item \textbf{Onde ela mora:} \texttt{core/include/retroforge/bootstrap/CompressionEngineFactory.hpp} e \texttt{core/src/bootstrap/CompressionEngineFactory.cpp} -- a mesma pasta \texttt{bootstrap/} que o Módulo 2 já criou para a \texttt{IdentificationEngineFactory}, exatamente como aquele módulo já previa (\textit{``o Módulo 3 vai introduzir, no mesmo espírito, uma \texttt{CompressionEngineFactory}, sem que esta classe [\texttt{IdentificationEngineFactory}] precise ser tocada''}) -- e, de fato, nenhuma linha de \texttt{IdentificationEngineFactory.hpp/.cpp} muda por causa deste módulo. +\end{itemize} \begin{CaixaCodigo} +// Arquivo: core/include/retroforge/bootstrap/CompressionEngineFactory.hpp +#pragma once + #include "retroforge/compression/CompressionRouter.hpp" + +namespace retroforge::bootstrap { + +// Responsabilidade unica: saber MONTAR o Motor de Recomendacao completo -- +// registrar, para cada Console suportado, a ICompressionStrategy correta. +// NAO decide COMO uma estrategia comprime -- isso continua sendo +// responsabilidade exclusiva de cada conversor concreto. Espelha +// IdentificationEngineFactory (Modulo 2) tanto na intencao quanto na +// pasta onde mora -- bootstrap/ e' reservada a classes de COMPOSICAO, +// nunca a regra de negocio em si. +class CompressionEngineFactory { +public: + // Constroi um CompressionRouter pronto para uso, com todas as rotas + // seguras ja registradas. DreamcastModificado e PSP_PBP nunca sao + // registrados aqui -- e nao precisam ser, ja que CompressionRouter:: + // rotear() os bloqueia antes de qualquer consulta ao mapa interno. + static compression::CompressionRouter criar(); +}; + +} // namespace retroforge::bootstrap +\end{CaixaCodigo} + +\begin{CaixaCodigo} +// Arquivo: core/src/bootstrap/CompressionEngineFactory.cpp +#include "retroforge/bootstrap/CompressionEngineFactory.hpp" + #include "retroforge/compression/converters/ChdmanConverter.hpp" #include "retroforge/compression/converters/DolphinToolConverter.hpp" -#include "retroforge/compression/converters/MaxcsoConverter.hpp" #include "retroforge/compression/converters/LibzipPacker.hpp" +#include "retroforge/compression/converters/MaxcsoConverter.hpp" -using namespace retroforge; -using models::Console; +#include -compression::CompressionRouter construir_motor_de_recomendacao() { +namespace retroforge::bootstrap { + +compression::CompressionRouter CompressionEngineFactory::criar() { compression::CompressionRouter roteador; + using models::Console; + + // Discos opticos -> chdman. O mesmo shared_ptr e' reaproveitado para + // as cinco rotas: ChdmanConverter nao guarda estado proprio entre + // chamadas, entao compartilhar a instancia evita alocacoes redundantes. auto chdman = std::make_shared(); roteador.register_strategy(Console::PS1, chdman); roteador.register_strategy(Console::PS2, chdman); @@ -504,43 +616,85 @@ \subsection{O coração do módulo: \texttt{CompressionRouter}} roteador.register_strategy(Console::Saturn, chdman); roteador.register_strategy(Console::Dreamcast, chdman); // .gdi limpo - roteador.register_strategy(Console::GameCube, - std::make_shared()); + // GameCube -> dolphin-tool. + roteador.register_strategy( + Console::GameCube, std::make_shared()); - // Console::PSP (.iso cru) vai para o maxcso -- Console::PSP_PBP NUNCA - // e' registrado aqui, propositalmente (ver CaixaAlerta acima). + // PSP cru -> maxcso. Console::PSP_PBP NUNCA e' registrado aqui, + // propositalmente (ver CompressionRouter::rotear). roteador.register_strategy(Console::PSP, - std::make_shared()); - - // Note: nem Console::DreamcastModificado nem Console::PSP_PBP sao - // registrados aqui -- e nenhum dos dois precisaria ser, ja que - // rotear() os bloqueia antes de consultar o mapa. A ausencia de - // registro e' redundante com a trava explicita, e essa redundancia - // e' proposital para os DOIS valores sentinela do projeto. + std::make_shared()); + + // Cartuchos -> libzip. Diferente da primeira versao deste modulo + // (quando LibzipPacker nao tinha nenhum detector correspondente + // ainda), o Modulo 2 ja entrega SnesDetector (Console::SNES, via + // extensao .smc/.sfc) e GenesisDetector (Console::Genesis, via + // assinatura binaria) prontos -- a lacuna documentada la esta + // fechada aqui, com o mesmo shared_ptr compartilhado entre as duas + // rotas, pelo mesmo motivo do chdman acima. + auto libzip = std::make_shared(); + roteador.register_strategy(Console::SNES, libzip); + roteador.register_strategy(Console::Genesis, libzip); + + // Nem Console::DreamcastModificado nem Console::PSP_PBP sao + // registrados aqui -- a trava explicita dentro de rotear() ja cobre + // os dois, e essa ausencia de registro e' redundante de proposito. return roteador; } + +} // namespace retroforge::bootstrap +\end{CaixaCodigo} + +Com a fábrica pronta, qualquer ponto de entrada -- o \texttt{main()} de demonstração deste módulo, o futuro \texttt{cli/main.cpp} (Módulo 11), ou o \texttt{BatchConversionManager} (Módulo 4) -- monta o motor completo com uma única chamada: + +\begin{CaixaCodigo} +#include + +#include "retroforge/bootstrap/CompressionEngineFactory.hpp" +#include "retroforge/bootstrap/IdentificationEngineFactory.hpp" + +using namespace retroforge; + +int main() { + auto identificador = bootstrap::IdentificationEngineFactory::criar(); // Modulo 2 + auto roteador = bootstrap::CompressionEngineFactory::criar(); // Modulo 3 + + auto console = identificador.identify("jogo.iso"); + + auto job = std::make_shared("jogo.iso", console); + auto resultado = roteador.rotear(job); + + switch (resultado) { + case compression::ResultadoRoteamento::ConversaoIniciada: + std::cout << "Conversao concluida com sucesso.\n"; + break; + case compression::ResultadoRoteamento::BloqueadoPorSeguranca: + std::cout << "Bloqueado por seguranca (Dreamcast .cdi ou PSP EBOOT.PBP).\n"; + break; + case compression::ResultadoRoteamento::ConsoleNaoSuportado: + std::cout << "Nenhuma rota registrada para este console, ou falha na compressao.\n"; + break; + } + + return 0; +} \end{CaixaCodigo} \begin{CaixaInfo} -Note que o mesmo \texttt{shared\_ptr} (\texttt{chdman}) é registrado para \texttt{PS1}, \texttt{PS2}, \texttt{SegaCD}, \texttt{Saturn} \textbf{e} \texttt{Dreamcast} -- as rotas baseadas em CD/DVD óptico compartilham a mesma instância da estratégia, porque \texttt{ChdmanConverter} não guarda nenhum estado próprio entre chamadas (cada \texttt{comprimir(job)} é independente). -Isso evita alocar objetos idênticos sem necessidade -- outro pequeno ganho que só é possível porque a classe foi desenhada, desde o início, sem estado mutável compartilhado indevidamente. +Note a simetria completa com o Módulo 2: assim como \texttt{cli/main.cpp} nunca vai precisar conhecer a lista inteira de detectores (isso é responsabilidade exclusiva de \texttt{IdentificationEngineFactory}), ele também nunca vai precisar conhecer a lista inteira de conversores (agora responsabilidade exclusiva de \texttt{CompressionEngineFactory}). As duas fábricas moram lado a lado em \texttt{bootstrap/}, cada uma cuidando de compor exatamente um motor -- nenhuma fábrica genérica que tentasse montar os dois de uma vez, o que a forçaria a depender de \texttt{identification/} \textbf{e} \texttt{compression/} simultaneamente, violando a Segregação de Interface no nível de módulo (o mesmo princípio já aplicado no Módulo 2 para justificar por que \texttt{bootstrap/} não ganha uma única fábrica ``faz-tudo''). \end{CaixaInfo} -\subsection{E o \texttt{LibzipPacker}, para qual console ele serve?} +\subsection{\texttt{LibzipPacker} não fica mais sem rota} -Se você conferir a montagem acima com atenção, vai notar algo importante: \textbf{nenhuma linha registra \texttt{LibzipPacker} no roteador}. -Isso não é um esquecimento -- é uma consequência honesta do estado atual do projeto. -A documentação v2.0 promete suporte a cartuchos e Arcade (\texttt{.sfc}, \texttt{.md}, coleções), mas o Módulo 2 \textbf{ainda não implementou} nenhum detector para esses formatos -- não existe, hoje, nenhum valor no \texttt{enum class Console} que represente ``cartucho de SNES'' ou ``coleção de Arcade'' com um detector associado (\texttt{Console::SNES} e \texttt{Console::MegaDrive} já existem no enum desde o Módulo 2, mas ainda sem nenhum \texttt{IConsoleDetector} ou \texttt{IContainerDetector} concreto que os produza). +A primeira versão deste módulo dedicava uma seção inteira a explicar, honestamente, por que \texttt{LibzipPacker} não tinha nenhuma rota registrada: o Módulo 2, na época, ainda não tinha nenhum \texttt{Console::SNES} ou \texttt{Console::MegaDrive} com detector concreto por trás. Isso mudou -- o Módulo 2 já formalizou \texttt{SnesDetector} (contêiner, extensão \texttt{.smc}/\texttt{.sfc}, produzindo \texttt{Console::SNES}) e \texttt{GenesisDetector} (assinatura binária, produzindo \texttt{Console::Genesis}) -- e a \texttt{CompressionEngineFactory} desta seção já fecha essa lacuna, registrando \texttt{LibzipPacker} para os dois. \begin{CaixaAlerta} -Isto é o Princípio Aberto/Fechado sendo exercitado em sua forma mais literal: \texttt{LibzipPacker} já está \textbf{pronto, testável e funcional} (Exercício 3.3), esperando por um detector correspondente que ainda não existe. Quando um módulo futuro de revisão do Motor de Identificação adicionar, por exemplo, um \texttt{SfcExtensionDetector} que produza \texttt{Console::SNES}, a \textbf{única} linha nova necessária em todo este módulo será um \texttt{roteador.register\_strategy(Console::SNES, std::make\_shared());} -- nenhuma classe já escrita aqui precisará ser reaberta. -Preferimos deixar essa lacuna explícita, documentada e discutida (Exercício 3.5), a inventar um detector neste módulo só para ``fechar a tabela'' artificialmente -- isso seria projetar sobre uma peça do Motor de Identificação que ainda não existe, invertendo a ordem correta de dependências do projeto (identificação vem antes de roteamento). +Vale registrar a diferença de nomenclatura entre o vocabulário do \texttt{enum class Console} (que usa \texttt{Genesis}, o nome ocidental do console) e a documentação original do projeto (que menciona ``\texttt{MegaDrive}'' em alguns trechos -- o nome usado fora da América do Norte para o mesmo hardware). Isso não é uma divergência de dois tipos diferentes -- é o mesmo console, um único valor de enum, com um nome de identificador escolhido e usado de forma consistente em todo o código (\texttt{GenesisDetector}, \texttt{Console::Genesis}). Documentação e código descrevendo a mesma coisa com sinônimos diferentes é aceitável; \textbf{dois valores de enum} para a mesma plataforma não seria -- e este projeto tem só um. \end{CaixaAlerta} \subsection{Arquitetura de Pastas: o que este módulo acrescenta} -O Módulo 0 já havia reservado o terreno (``cada subpasta corresponde a uma área de responsabilidade''); -este módulo é a primeira vez que uma \textbf{pasta inteiramente nova} nasce sob \texttt{core/}, ao lado de \texttt{io/}, \texttt{process/}, \texttt{identification/}, \texttt{models/} e \texttt{bootstrap/} (esta última já introduzida pelo Módulo 2): +O Módulo 0 já havia reservado o terreno (``cada subpasta corresponde a uma área de responsabilidade''). Este módulo acrescenta \texttt{compression/} (a primeira pasta inteiramente nova de regra de negócio desde \texttt{identification/}) e estende \texttt{bootstrap/}, que o Módulo 2 já havia criado: \begin{CaixaArvore} core/ @@ -548,40 +702,35 @@ \subsection{Arquitetura de Pastas: o que este módulo acrescenta} | |-- io/ [Modulo 0, sem alteracoes] | |-- process/ [Modulo 0, reaproveitado neste modulo] | |-- identification/ [Modulo 0 e 2, sem alteracoes neste modulo] -| |-- bootstrap/ [Modulo 2, sem alteracoes neste modulo] -| |-- models/ [Modulo 0/2, sem alteracoes neste modulo -- -| | Console.hpp ja unificado desde o Modulo 0] +| |-- models/ [Modulo 0/2, sem alteracoes] +| |-- bootstrap/ +| | |-- IdentificationEngineFactory.hpp [Modulo 2, sem alteracoes] +| | `-- CompressionEngineFactory.hpp [NOVO neste modulo] | `-- compression/ [NOVO neste modulo] | |-- ICompressionStrategy.hpp [NOVO] | |-- CompressionRouter.hpp [NOVO] | `-- converters/ [NOVO -- espelha identification/detectors/] | |-- ChdmanConverter.hpp [NOVO] | |-- DolphinToolConverter.hpp [NOVO] -| |-- MaxcsoConverter.hpp [NOVO -- Exercicio 3.1] +| |-- MaxcsoConverter.hpp [NOVO] | `-- LibzipPacker.hpp [NOVO] `-- src/ - |-- models/ - | `-- ConversionJob.cpp [Modulo 0, sem alteracoes de logica] + |-- bootstrap/ + | |-- IdentificationEngineFactory.cpp [Modulo 2, sem alteracoes] + | `-- CompressionEngineFactory.cpp [NOVO neste modulo] `-- compression/ [NOVO neste modulo] - |-- CompressionRouter.cpp [NOVO -- ver CaixaAlerta sobre PSP_PBP] - `-- converters/ + |-- CompressionRouter.cpp [NOVO] + `-- converters/ [NOVO -- NAO "detectors/", ver alerta acima] |-- ChdmanConverter.cpp [NOVO] |-- DolphinToolConverter.cpp [NOVO] - |-- MaxcsoConverter.cpp [NOVO -- Exercicio 3.1] + |-- MaxcsoConverter.cpp [NOVO] `-- LibzipPacker.cpp [NOVO] \end{CaixaArvore} \begin{CaixaInfo} -Graças ao \texttt{file(GLOB\_RECURSE ... CONFIGURE\_DEPENDS)} já configurado no Módulo 1 para \texttt{core/CMakeLists.txt}, toda a pasta \texttt{compression/} entra automaticamente no build de \texttt{retroforge\_core} assim que os arquivos forem salvos em disco -- nenhuma linha do \texttt{CMakeLists.txt} raiz ou de \texttt{core/CMakeLists.txt} precisa ser tocada por causa da nova subpasta. -A única linha de build genuinamente pendente é a que resolve a dependência de \texttt{libzip} (\texttt{find\_package} ou \texttt{FetchContent}, Seção 3.5 deste módulo) -- essa, sim, exige uma edição real em \texttt{core/CMakeLists.txt}, porque \texttt{libzip} não é header-only como o \texttt{CLI11}. +Graças ao \texttt{file(GLOB\_RECURSE ... CONFIGURE\_DEPENDS)} já configurado no Módulo 1 para \texttt{core/CMakeLists.txt}, toda a pasta \texttt{compression/} entra automaticamente no build de \texttt{retroforge\_core} assim que os arquivos forem salvos em disco -- nenhuma linha do \texttt{CMakeLists.txt} raiz ou de \texttt{core/CMakeLists.txt} precisa ser tocada por causa da nova subpasta. A única linha de build genuinamente pendente é a que resolve a dependência de \texttt{libzip} (Seção sobre \texttt{LibzipPacker} acima) -- essa, sim, exige uma edição real em \texttt{core/CMakeLists.txt} e no workflow de CI, porque \texttt{libzip} não é header-only como o \texttt{CLI11}. \end{CaixaInfo} -\subsection{Onde o projeto está agora, de fato} - -Vale fechar este módulo com um retrato honesto do estado do repositório \texttt{Dev-Sams1012/RetroForge} no momento em que este texto foi escrito: a raiz contém \texttt{.github/workflows/}, \texttt{cli/}, \texttt{core/} (já com \texttt{identification/} e \texttt{bootstrap/} completos do Módulo 2), \texttt{external/}, além dos arquivos de configuração (\texttt{CMakeLists.txt}, \texttt{.gitmodules}, \texttt{.clang-format}, \texttt{.clangd}, \texttt{.gitignore}) já dissecados nos Módulos 0 e 1. Ou seja: tudo o que este Módulo 3 descreveu -- \texttt{compression/}, os quatro conversores e o \texttt{CompressionRouter} com a trava dupla -- ainda \textbf{não foi commitado}. -Isso é esperado e intencional: este ``livro'' funciona como um roteiro de implementação incremental, e o repositório real avança módulo a módulo, na mesma ordem em que este texto é escrito. -Ao terminar os exercícios deste módulo, o próximo passo prático (fora do livro) é abrir um Pull Request que adicione exatamente essas pastas e classes ao repositório, mantendo o CI (Módulo 1) verde -- \texttt{clang-format --dry-run --Werror} incluso. - \subsection{Exercícios Práticos do Módulo 3} \begin{CaixaDesafio} @@ -589,34 +738,27 @@ \subsection{Exercícios Práticos do Módulo 3} \textbf{[laboratório]} = validação prática descartável (não precisa sobreviver no repositório depois de feito). \begin{enumerate}[label=\textbf{3.\arabic*}] - \item \textbf{[projeto]} Implemente \texttt{MaxcsoConverter.hpp}/\texttt{.cpp} seguindo exatamente o molde de \texttt{ChdmanConverter}, montando o comando \texttt{maxcso "origem" -o "destino"} e retornando \texttt{".cso"} em \texttt{extensao\_saida()}. -Registre-a no \texttt{CompressionRouter} para \texttt{Console::PSP} -- \textbf{nunca} para \texttt{Console::PSP\_PBP}. - \item \textbf{[laboratório]} Sem rodar um jogo real: crie um \texttt{main()} temporário que monte o \texttt{CompressionRouter} completo, construa manualmente quatro \texttt{ConversionJob} fictícios (\texttt{Console::PS2}, \texttt{Console::GameCube}, \texttt{Console::DreamcastModificado} e \texttt{Console::PSP\_PBP} -- apontando para caminhos que nem precisam existir de verdade) e confirme, olhando apenas o valor de \texttt{ResultadoRoteamento} devolvido, que o terceiro \textbf{e} o quarto são bloqueados \textbf{antes} de qualquer tentativa de chamar um subprocesso. -Este \texttt{main()} é descartável -- será formalizado como teste automatizado de verdade no Módulo 12. - \item \textbf{[projeto]} Implemente \texttt{LibzipPacker.hpp}/\texttt{.cpp} conforme apresentado nesta seção. -Mesmo sem nenhuma rota registrada ainda no \texttt{CompressionRouter} (Seção 3.7), escreva um pequeno teste manual que chame \texttt{LibzipPacker::comprimir} diretamente sobre um \texttt{ConversionJob} fictício apontando para um arquivo de texto qualquer, e confirme que um \texttt{.zip} válido é gerado ao final. -A classe é entregável permanente; o teste manual de confirmação é descartável. - \item \textbf{[laboratório]} Escreva um pequeno arquivo de verificação (não precisa virar parte permanente do build) contendo apenas \texttt{static\_assert(std::is\_same\_v, "...")}\texttt{;}. -Confirme que ele compila sem erro, e explique em poucas frases por que isso prova que \texttt{CompressionRouter} (que usa \texttt{models::Console}) e \texttt{ConsoleIdentifier} (que devolve \texttt{identification::Console}) nunca podem divergir silenciosamente -- a unificação feita no Módulo 0 (\texttt{models/Console.hpp} como única fonte de verdade) é o que torna essa prova trivial. - \item \textbf{[laboratório]} Discuta por escrito (não precisa codificar): que novo(s) detector(es) de contêiner (Módulo 2) seriam necessários para que \texttt{LibzipPacker} finalmente ganhe uma rota registrada no \texttt{CompressionRouter} para os valores \texttt{Console::SNES} e \texttt{Console::MegaDrive}, que já existem no enum mas ainda não têm detector? -Uma única classe de detector bastaria para os dois formatos, ou o Princípio da Responsabilidade Única sugere duas classes distintas, como \texttt{DreamcastGdiDetector} e \texttt{DreamcastCdiDetector} fizeram no Módulo 2? -É uma reflexão escrita -- guarde a resposta para quando o Motor de Identificação for revisitado. - \item \textbf{[laboratório]} Compare, com um pequeno teste manual, o comportamento de \texttt{CompressionRouter::rotear} quando um \texttt{ConversionJob} é criado com \texttt{Console::Desconhecido} (o valor que \texttt{ConsoleIdentifier::identify} devolve quando nenhum detector reconhece o arquivo, Módulo 2). -Confirme que o resultado é \texttt{ResultadoRoteamento::ConsoleNaoSuportado}, e não um bloqueio de segurança -- são três situações semanticamente diferentes agora (``não sei o que é isso'' vs.\ ``sei o que é isso e é perigoso'' vs.\ ``sei o que é isso e é seguro''), e vale confirmar que o código realmente distingue todas elas, inclusive entre os dois casos de bloqueio (\texttt{DreamcastModificado} e \texttt{PSP\_PBP}). + \item \textbf{[projeto]} Implemente \texttt{CompressionEngineFactory.hpp}/\texttt{.cpp} conforme apresentado nesta seção, dentro de \texttt{bootstrap/}, ao lado de \texttt{IdentificationEngineFactory}. Se você já tinha uma função solta \texttt{construir\_motor\_de\_recomendacao()} em algum arquivo de demonstração, remova-a -- a fábrica é o único ponto de composição do motor de recomendação a partir de agora. + \item \textbf{[laboratório]} Sem rodar um jogo real: crie um \texttt{main()} temporário que monte o \texttt{CompressionRouter} via \texttt{CompressionEngineFactory::criar()}, construa manualmente seis \texttt{ConversionJob} fictícios (\texttt{Console::PS2}, \texttt{Console::GameCube}, \texttt{Console::SNES}, \texttt{Console::Genesis}, \texttt{Console::DreamcastModificado} e \texttt{Console::PSP\_PBP}) e confirme, olhando apenas o valor de \texttt{ResultadoRoteamento} devolvido, que os dois últimos são bloqueados \textbf{antes} de qualquer tentativa de chamar um subprocesso ou tocar o \texttt{libzip}. Este \texttt{main()} é descartável -- será formalizado como teste automatizado de verdade no Módulo 12. + \item \textbf{[projeto]} Confirme que \texttt{core/CMakeLists.txt} resolve \texttt{libzip} corretamente tanto no seu ambiente Linux (via \texttt{pkg-config}) quanto -- se você tiver acesso a uma máquina ou VM Windows -- via \texttt{vcpkg}/\texttt{find\_package(libzip CONFIG REQUIRED)}. Atualize também o workflow de CI para instalar \texttt{pkg-config} e \texttt{libzip-dev} antes da checagem de formatação. + \item \textbf{[laboratório]} Escreva um teste manual que chame \texttt{LibzipPacker::comprimir} diretamente sobre um \texttt{ConversionJob} fictício apontando para um arquivo de texto qualquer, e confirme que um \texttt{.zip} válido é gerado ao final. Em seguida, force uma falha deliberada (por exemplo, apontando para um arquivo que não existe) e confirme que \textbf{nenhum} arquivo \texttt{.zip} corrompido ou parcial fica no disco -- prova de que o \texttt{zip\_discard()} no destrutor de \texttt{ScopedZipHandle} está funcionando como esperado. + \item \textbf{[laboratório]} Corrija, no seu clone, a mensagem de erro do \texttt{catch} em \texttt{MaxcsoConverter::comprimir} (Seção sobre os conversores de subprocesso) caso ela ainda mencione ``chdman'' em vez de ``maxcso''. Depois, revise \texttt{ChdmanConverter} e \texttt{DolphinToolConverter} à procura do mesmo tipo de mensagem copiada por engano. + \item \textbf{[laboratório]} Confirme que os arquivos \texttt{.cpp} deste módulo estão em \texttt{core/src/compression/converters/}, e não em \texttt{core/src/compression/detectors/}. Se encontrar a pasta errada no seu clone, mova os arquivos e confirme que o projeto ainda compila (o \texttt{CONFIGURE\_DEPENDS} do Módulo 1 vai encontrá-los automaticamente no novo caminho). + \item \textbf{[laboratório]} Compare, com um pequeno teste manual, o comportamento de \texttt{CompressionRouter::rotear} quando um \texttt{ConversionJob} é criado com \texttt{Console::Desconhecido}. Confirme que o resultado é \texttt{ResultadoRoteamento::ConsoleNaoSuportado}, e não um bloqueio de segurança -- três situações semanticamente diferentes, e vale confirmar que o código realmente as distingue, inclusive entre os dois casos de bloqueio (\texttt{DreamcastModificado} e \texttt{PSP\_PBP}). \end{enumerate} \end{CaixaDesafio} \begin{CaixaResumo} Neste módulo você aprendeu: \begin{itemize} - \item Por que o roteamento de compressão (Smart Context) merece ser uma classe própria, separada tanto da identificação (Módulo 2) quanto da apresentação (\texttt{cli/}/\texttt{gui/}) -- Responsabilidade Única aplicada à orquestração, não só às classes de dados. - \item Como uma única interface (\texttt{ICompressionStrategy}) pode, corretamente, unificar duas naturezas de integração completamente diferentes -- subprocesso (\texttt{chdman}, \texttt{maxcso}, \texttt{dolphin-tool}) e biblioteca estática (\texttt{libzip}) -- porque o contrato observável é idêntico, mesmo com mecanismos internos radicalmente distintos. - \item Como reaproveitar, sem alterar, duas classes do Módulo 0 (\texttt{process::ExternalProcessHandle} e o \texttt{shared\_ptr}) em um contexto totalmente novo -- a prova prática de que investir em RAII e posse bem definida logo no início do projeto paga dividendos em módulos futuros. - \item Por que valeu a pena, lá no Módulo 0 e expandido no Módulo 2, extrair o \texttt{enum class Console} para seu próprio arquivo neutro (\texttt{models/Console.hpp}) em vez de declará-lo em dependências isoladas: \texttt{compression/} pôde incluir apenas o tipo de que precisa para mapear rotas incluindo \texttt{SegaCD}, \texttt{Saturn} e as duas travas de segurança, sem se acoplar à interface de identificação inteira. - \item Que \texttt{Console::DreamcastModificado} e \texttt{Console::PSP\_PBP} são, arquiteturalmente, o \textbf{mesmo tipo de problema} -- e por que a correção deste módulo foi estender a trava explícita e antecipada, verificada antes do \texttt{unordered\_map}, para cobrir \textbf{ambos} os valores sentinela, em vez de depender apenas da ausência de registro no mapa de estratégias. - \item Que uma estratégia de compressão pode estar 100\% implementada e testável (\texttt{LibzipPacker}) mesmo antes de qualquer detector de console alimentá-la -- e por que isso é uma consequência saudável do Princípio Aberto/Fechado, não uma lacuna a ser preenchida às pressas. + \item Por que o roteamento de compressão (Smart Context) merece ser uma classe própria, separada tanto da identificação (Módulo 2) quanto da apresentação (\texttt{cli/}/\texttt{gui/}). + \item Como uma única interface (\texttt{ICompressionStrategy}) pode, corretamente, unificar duas naturezas de integração completamente diferentes -- subprocesso e biblioteca estática -- porque o contrato observável é idêntico, mesmo com mecanismos internos radicalmente distintos. + \item Como reaproveitar, sem alterar, duas classes do Módulo 0 (\texttt{process::ExternalProcessHandle} e o \texttt{shared\_ptr}) em um contexto totalmente novo. + \item Que \texttt{Console::DreamcastModificado} e \texttt{Console::PSP\_PBP} são, arquiteturalmente, o mesmo tipo de problema -- e por que a trava explícita, verificada antes do \texttt{unordered\_map}, cobre ambos. + \item Por que a \textbf{montagem} do motor de recomendação merece sua própria classe -- \texttt{CompressionEngineFactory}, na pasta \texttt{bootstrap/} já criada pelo Módulo 2 -- em vez de viver como função solta: mesma motivação, mesmo padrão, e mesma pasta que \texttt{IdentificationEngineFactory} já estabeleceu, evitando duplicação futura entre \texttt{cli/} e \texttt{gui/} e mantendo a composição testável isoladamente (Módulo 12). + \item Que \texttt{LibzipPacker} não fica mais sem rota registrada: os detectores de cartucho que o Módulo 2 já entrega (\texttt{SnesDetector}, \texttt{GenesisDetector}) fecham a lacuna que a primeira versão deste módulo apenas documentava como pendente. + \item Como resolver \texttt{libzip} de forma multiplataforma desde já (\texttt{pkg-config} no Linux, \texttt{find\_package}/\texttt{vcpkg} no Windows), unificados atrás de um único alvo lógico \texttt{libzip::zip} -- e por que o padrão RAII de \texttt{LibzipPacker} usa \texttt{zip\_discard()} como destrutor padrão, promovendo para \texttt{zip\_close()} explícito só no caminho de sucesso confirmado. \end{itemize} -Com o console identificado \textbf{e} roteado com segurança para a ferramenta certa (ou explicitamente bloqueado, nos dois casos sentinela), falta resolver um problema que a documentação (Seção 3.4) já anunciou desde o início: converter 50 jogos, um de cada vez, de forma sequencial, é lento e desperdiça os múltiplos núcleos de qualquer máquina moderna. -O Módulo 4 vai introduzir o Thread Pool customizado do RetroForge -- e é exatamente o \texttt{CompressionRouter} deste módulo, já com sua trava dupla, que cada \textit{worker} da piscina de threads vai chamar, em paralelo, para múltiplos \texttt{ConversionJob} simultâneos. +Com o console identificado \textbf{e} roteado com segurança para a ferramenta certa (ou explicitamente bloqueado, nos dois casos sentinela) -- agora incluindo cartuchos de verdade --, falta resolver um problema que a documentação (Seção 3.4) já anunciou desde o início: converter 50 jogos, um de cada vez, de forma sequencial, é lento e desperdiça os múltiplos núcleos de qualquer máquina moderna. O Módulo 4 vai introduzir o Thread Pool customizado do RetroForge -- e é exatamente o \texttt{CompressionRouter} deste módulo, montado agora por \texttt{CompressionEngineFactory}, que cada \textit{worker} da piscina de threads vai chamar, em paralelo, para múltiplos \texttt{ConversionJob} simultâneos. \end{CaixaResumo} diff --git a/docs/RetroForge_Modulo4.tex b/docs/RetroForge_Modulo4.tex index cdb61ea..23fe789 100644 --- a/docs/RetroForge_Modulo4.tex +++ b/docs/RetroForge_Modulo4.tex @@ -202,7 +202,7 @@ \subsection{\texttt{BatchConversionManager}: a fila inteligente (US03/US08)} \begin{itemize} \item \textbf{Por que \texttt{BatchConversionManager} existe:} é a única classe que sabe que ``processar um lote'' significa ``para cada \texttt{ConversionJob}, enfileirar uma tarefa que chama \texttt{CompressionRouter::rotear}''. Nem a \texttt{ThreadPool} (que não sabe o que é um console) nem o \texttt{CompressionRouter} (que não sabe que existe uma fila) têm essa responsabilidade -- ela pertence exclusivamente a esta classe. - \item \textbf{Como se comunica com outras classes:} depende de \texttt{compression::CompressionRouter} (por referência -- não é dona dele; o \texttt{CompressionRouter} é montado uma única vez, fora daqui, como já vimos no Módulo 3, já com sua trava dupla contra \texttt{DreamcastModificado} e \texttt{PSP\_PBP}) e possui, por composição, sua própria \texttt{concurrency::ThreadPool}. É consumida pela camada de apresentação (\texttt{cli/}, e futuramente \texttt{gui/}), que é quem efetivamente chama \texttt{adicionar\_job} para cada arquivo encontrado (Módulo 7) e \texttt{cancelar\_job} quando o usuário pede. + \item \textbf{Como se comunica com outras classes:} depende de \texttt{compression::CompressionRouter} (por referência -- não é dona dele; o \texttt{CompressionRouter} é montado uma única vez, fora daqui, via \texttt{bootstrap::CompressionEngineFactory::criar()} (Módulo 3, revisado), já com sua trava dupla contra \texttt{DreamcastModificado} e \texttt{PSP\_PBP} e com as rotas de \texttt{libzip} para \texttt{SNES}/\texttt{Genesis}) e possui, por composição, sua própria \texttt{concurrency::ThreadPool}. É consumida pela camada de apresentação (\texttt{cli/}, e futuramente \texttt{gui/}), que é quem efetivamente chama \texttt{adicionar\_job} para cada arquivo encontrado (Módulo 7) e \texttt{cancelar\_job} quando o usuário pede. \item \textbf{Onde ela mora:} \texttt{core/include/retroforge/batch/BatchConversionManager.hpp} e \texttt{core/src/batch/BatchConversionManager.cpp} -- uma segunda pasta nova, \texttt{batch/}, que representa a área de responsabilidade ``orquestração de lote'', distinta tanto de \texttt{concurrency/} (mecanismo genérico) quanto de \texttt{compression/} (regra de roteamento). \end{itemize} @@ -557,16 +557,23 @@ \subsection{Montando o motor completo} #include #include "retroforge/batch/BatchConversionManager.hpp" +#include "retroforge/bootstrap/CompressionEngineFactory.hpp" #include "retroforge/concurrency/monitors/LinuxProcStatCpuMonitor.hpp" -// ... includes do Modulo 3 (construir_motor_de_recomendacao) omitidos using namespace retroforge; int main() { - auto roteador = construir_motor_de_recomendacao(); // Modulo 3 -- ja com a - // trava dupla contra - // DreamcastModificado - // e PSP_PBP + auto roteador = bootstrap::CompressionEngineFactory::criar(); // Modulo 3 + // (revisado) -- + // ja com a + // trava dupla + // contra + // DreamcastModificado + // e PSP_PBP, + // e com as + // rotas de + // libzip para + // SNES/Genesis auto monitor_cpu = std::make_shared(); diff --git a/docs/RetroForge_Modulo5.tex b/docs/RetroForge_Modulo5.tex new file mode 100644 index 0000000..4555145 --- /dev/null +++ b/docs/RetroForge_Modulo5.tex @@ -0,0 +1,669 @@ +\section{Módulo 5: Pré-Cálculos e Scrapers (Estimativas Inteligentes)} + +Os Módulos 2, 3 e 4 já resolvem, em conjunto, três perguntas: \textit{``que console é esse arquivo?''}, \textit{``qual ferramenta comprime isso, ou isso deve ser bloqueado?''} e \textit{``como processar 50 desses ao mesmo tempo, com segurança?''}. Mas antes de o usuário apertar ``Iniciar'' sobre uma pasta de 50 jogos (US03: ``ver uma estimativa de quantos gigabytes irei economizar... antes de apertar `Iniciar'''), a documentação promete duas coisas que nenhuma classe do projeto ainda entrega: um número (\textit{quantos GB isso vai economizar?}) e, sob demanda, um nome oficial (Seção 3.6 da documentação: o Scraper Online de Metadados). Este módulo constrói as duas. + +\begin{CaixaInfo}[title={O que já existe vs. o que este módulo constrói}] +Dos Módulos 0 a 4, já temos prontos: \texttt{models::Console} (vocabulário completo, incluindo os dois sentinelas), \texttt{models::ConversionJob}, \texttt{compression::ICompressionStrategy}/\texttt{CompressionRouter} (cada estratégia já expõe \texttt{extensao\_saida()}, que vamos reaproveitar aqui pela primeira vez fora do Módulo 3), e \texttt{concurrency::ThreadPool} (Módulo 4), que este módulo reaproveita \textbf{sem alterar uma linha} para paralelizar hashing em lote -- exatamente a previsão feita ao final do Módulo 4. + +No repositório público (\texttt{github.com/Dev-Sams1012/RetroForge}), nada relacionado a estimativa ou scraping existe ainda: não há pasta \texttt{estimation/} nem \texttt{scraping/} dentro de \texttt{core/include/retroforge/}, nenhuma dependência de \texttt{xxHash} ou \texttt{libcurl} foi adicionada ao \texttt{core/CMakeLists.txt}, e nenhuma chamada de rede foi escrita em lugar nenhum do projeto. Este módulo entrega: (1) o \texttt{StorageEstimator}, que aplica os multiplicadores da Seção 3.3 da documentação sobre o tamanho real do arquivo (via \texttt{std::filesystem::file\_size}, sem abrir o arquivo); (2) a interface \texttt{IHashCalculator} e sua implementação \texttt{XxHashCalculator}, que faz hashing em blocos (retomando a técnica de \textit{chunks} do Módulo 0); (3) o \texttt{MetadataScraperClient}, que consome uma API pública via \texttt{libcurl} apenas quando explicitamente acionado (Seção 3.6: ``Apenas se o usuário clicar em `Renomear / Buscar Metadados'''); e (4) o \texttt{BatchHashScanner}, que reaproveita \texttt{concurrency::ThreadPool} para hashear múltiplos arquivos em paralelo. +\end{CaixaInfo} + +\subsection{Duas responsabilidades, deliberadamente separadas} + +É tentador projetar uma única classe ``\texttt{MetadataAndSizeHelper}'' que calcula tamanho \textbf{e} busca metadados online. O Módulo 2 já rejeitou essa tentação uma vez, ao separar \texttt{IContainerDetector} de \texttt{IContainerResolver}; aqui o motivo é o mesmo, só que entre \textbf{estimativa} e \textbf{scraping}: + +\begin{itemize} + \item \textbf{Estimativa de armazenamento} (Seção 3.3 da documentação) é pura aritmética sobre um número já disponível (\texttt{std::filesystem::file\_size}) e o \texttt{Console} já identificado pelo Módulo 2. Não toca a rede, não lê um único byte do arquivo, e roda \textbf{sempre}, para todo arquivo da fila. + \item \textbf{Scraping de metadados} (Seção 3.6) exige ler o arquivo inteiro em blocos para gerar uma hash, e depois consultar uma API externa -- caro, opcional, e só executado \textbf{sob demanda} (``Apenas se o usuário clicar em...''). +\end{itemize} + +\begin{CaixaAlerta} +Misturar as duas responsabilidades em uma classe faria toda estimativa de tamanho (barata, sempre executada) carregar, sem necessidade, a complexidade e as dependências (\texttt{libcurl}, hashing de arquivo inteiro) de uma operação cara e opcional. US03 (estimativa) e a US derivada de scraping da Seção 3.6 têm ciclos de vida completamente diferentes -- uma acontece \textbf{antes} de qualquer clique do usuário, a outra só \textbf{depois} de um clique explícito. Isso é Responsabilidade Única aplicada não só a uma classe, mas a uma \textbf{área inteira} do projeto: por isso nascem duas pastas novas, \texttt{estimation/} e \texttt{scraping/}, nunca uma pasta única ``\texttt{metadata/}'' genérica. +\end{CaixaAlerta} + +\subsection{Estimativa Preditiva de Armazenamento: \texttt{StorageEstimator}} + +A Seção 3.3 da documentação define a regra com precisão: multiplicadores médios por rota de compressão, aplicados sobre o tamanho original do arquivo. Como o \texttt{Console} de um job já foi resolvido pelo Módulo 2, e a rota de compressão (e, portanto, o multiplicador correto) é exatamente a mesma tabela que o \texttt{CompressionRouter} usa para \textbf{rotear} -- só que aqui usada para \textbf{estimar}, não para executar. + +\begin{itemize} + \item \textbf{Por que \texttt{StorageEstimator} existe:} sua única responsabilidade é traduzir ``este arquivo, deste tamanho, deste console'' em ``este tamanho final estimado, em bytes''. Ela não comprime nada, não abre o arquivo, e não sabe nada sobre \texttt{ICompressionStrategy} -- apenas sobre a tabela de multiplicadores da documentação (Seção 3.3: -40\% CD$\to$CHD, -30\% UMD$\to$CSO, e o -30\% de RVZ citado na Seção 3.3 da v2.0). + \item \textbf{Como se comunica com outras classes:} recebe apenas um \texttt{models::Console} e um \texttt{std::uintmax\_t} (tamanho original) -- \textbf{nunca} um \texttt{ConversionJob} inteiro nem um \texttt{ICompressionStrategy}. Isso é deliberado: acoplar \texttt{StorageEstimator} a \texttt{ConversionJob} obrigaria toda estimativa em lote (Seção~\ref{sec:batch-estimator}) a construir jobs completos só para perguntar um tamanho -- um acoplamento desnecessário que o Módulo 7 (varredura de diretórios) teria que desfazer mais tarde. + \item \textbf{Onde ela mora:} \texttt{core/include/retroforge/estimation/StorageEstimator.hpp} e \texttt{core/src/estimation/StorageEstimator.cpp} -- uma pasta nova, \texttt{estimation/}, irmã de \texttt{compression/}, \texttt{concurrency/} e \texttt{batch/}. +\end{itemize} + +\begin{CaixaCodigo} +// Arquivo: core/include/retroforge/estimation/StorageEstimator.hpp +#pragma once + +#include + +#include "retroforge/models/Console.hpp" + +namespace retroforge::estimation { + +// Resultado da estimativa: tamanho estimado apos a compressao, mais o +// tamanho original (para calcular a economia percentual na interface). +struct EstimativaArmazenamento { + std::uintmax_t tamanho_original = 0; + std::uintmax_t tamanho_estimado = 0; +}; + +// Responsabilidade unica: aplicar os multiplicadores medios da Secao 3.3 +// da documentacao sobre um tamanho de arquivo ja conhecido. NAO abre o +// arquivo, NAO sabe comprimir de verdade (isso e' compression::), e NAO +// sabe identificar o console (isso ja aconteceu no Modulo 2). +class StorageEstimator { +public: + EstimativaArmazenamento estimar(models::Console console, + std::uintmax_t tamanho_original) const; + +private: + // Multiplicador (0.0 a 1.0) representando a FRACAO do tamanho + // original que sobrevive apos a compressao. Ex: 0.60 para CHD + // (documentacao: "-40% para jogos em CD convertidos para CHD"). + double multiplicador_para(models::Console console) const; +}; + +} // namespace retroforge::estimation +\end{CaixaCodigo} + +\begin{CaixaCodigo} +// Arquivo: core/src/estimation/StorageEstimator.cpp +#include "retroforge/estimation/StorageEstimator.hpp" + +namespace retroforge::estimation { + +using models::Console; + +double StorageEstimator::multiplicador_para(Console console) const { + switch (console) { + // Discos opticos -> CHD (documentacao Secao 3.3: -40% a -45% + // dependendo do console; usamos 0.60 como media conservadora). + case Console::PS1: + case Console::PS2: + case Console::SegaCD: + case Console::Saturn: + case Console::Dreamcast: + return 0.60; + + // GameCube -> RVZ (documentacao v2.0 Secao 3.3: -30%). + case Console::GameCube: + return 0.70; + + // PSP cru -> CSO (documentacao Secao 3.3: -30% para UMDs). + case Console::PSP: + return 0.70; + + // Cartuchos ja sao pequenos e pouco compressiveis; .zip + // costuma economizar muito pouco -- estimativa conservadora. + case Console::SNES: + case Console::MegaDrive: + return 0.95; + + // Casos bloqueados por seguranca (Modulo 3) ou desconhecidos: + // NAO ha compressao real, entao o tamanho estimado e' o mesmo + // que o original -- nunca prometemos uma economia que nao + // vai acontecer. + case Console::DreamcastModificado: + case Console::PSP_PBP: + case Console::Desconhecido: + default: + return 1.0; + } +} + +EstimativaArmazenamento StorageEstimator::estimar(Console console, + std::uintmax_t tamanho_original) const { + double fracao = multiplicador_para(console); + + EstimativaArmazenamento resultado; + resultado.tamanho_original = tamanho_original; + resultado.tamanho_estimado = + static_cast(static_cast(tamanho_original) * fracao); + + return resultado; +} + +} // namespace retroforge::estimation +\end{CaixaCodigo} + +\begin{CaixaAlerta} +Repare o \texttt{switch} explícito sobre \texttt{Console}, cobrindo \textbf{todos} os valores do enum -- inclusive \texttt{DreamcastModificado} e \texttt{PSP\_PBP}, que devolvem \texttt{1.0} (nenhuma economia prometida) em vez de cair silenciosamente em um \texttt{default} genérico. Isso é o mesmo hábito estabelecido no Módulo 2 (\texttt{-Wswitch} forçando todo \texttt{switch} futuro a tratar cada valor sentinela): se o \texttt{enum class Console} ganhar um console novo em um módulo futuro, o compilador avisa que \texttt{StorageEstimator::multiplicador\_para} também precisa de uma decisão explícita -- nunca herda, por acidente, o comportamento do \texttt{default}. +\end{CaixaAlerta} + +\subsection{Estimativa em lote: reaproveitando \texttt{ThreadPool} sem herdar dependências de compressão} +\label{sec:batch-estimator} + +A US03 fala em ``arrastar uma pasta com 50 jogos'' -- calcular 50 tamanhos de arquivo é uma operação de I/O rápida (apenas metadados do sistema de arquivos, não leitura de conteúdo), mas ainda assim vale paralelizar quando a pasta está em um HD externo lento. Aqui aparece o primeiro pagamento real da separação que o Módulo 4 fez entre \texttt{concurrency/} (genérica) e \texttt{batch/} (específica de domínio): \texttt{concurrency::ThreadPool} pode ser usada por uma classe de estimativa \textbf{sem} que essa classe precise depender de \texttt{compression::CompressionRouter}. + +\begin{itemize} + \item \textbf{Por que \texttt{BatchStorageEstimator} existe:} orquestra \texttt{StorageEstimator} sobre uma lista de caminhos, usando \texttt{concurrency::ThreadPool} para paralelizar as chamadas a \texttt{std::filesystem::file\_size}. Sua responsabilidade termina ao devolver uma lista de \texttt{EstimativaArmazenamento} -- ela não decide rotas de compressão, não converte nada. + \item \textbf{Como se comunica com outras classes:} depende de \texttt{estimation::StorageEstimator} (composição direta, sem estado próprio compartilhado) e de \texttt{concurrency::ThreadPool} -- a mesma classe genérica do Módulo 4, sem nenhuma alteração. \textbf{Não} depende de \texttt{compression::CompressionRouter} nem de \texttt{models::ConversionJob} -- a prova concreta de que a divisão \texttt{concurrency/} vs.\ \texttt{compression/} vs.\ \texttt{batch/} do Módulo 4 realmente evitou o acoplamento que pareceria natural. + \item \textbf{Onde ela mora:} \texttt{core/include/retroforge/estimation/BatchStorageEstimator.hpp} e seu \texttt{.cpp} correspondente, ao lado de \texttt{StorageEstimator.hpp}. +\end{itemize} + +\begin{CaixaCodigo} +// Arquivo: core/include/retroforge/estimation/BatchStorageEstimator.hpp +#pragma once + +#include +#include +#include + +#include "retroforge/concurrency/ThreadPool.hpp" +#include "retroforge/estimation/StorageEstimator.hpp" +#include "retroforge/models/Console.hpp" + +namespace retroforge::estimation { + +// Um item de entrada: caminho + console ja identificado pelo Modulo 2. +// Deliberadamente NAO e' um models::ConversionJob -- ver justificativa +// na Secao 5.4 sobre por que StorageEstimator nao depende de ConversionJob. +struct ItemParaEstimativa { + std::filesystem::path caminho; + models::Console console; +}; + +// Reaproveita concurrency::ThreadPool (Modulo 4) SEM alterar uma linha +// dela e SEM depender de compression:: -- a prova de que a separacao +// entre "mecanismo generico" e "orquestracao de dominio" do Modulo 4 +// permite reuso em um contexto completamente novo (estimativa, nao +// compressao). +class BatchStorageEstimator { +public: + explicit BatchStorageEstimator(size_t numero_de_threads); + + std::vector + estimar_lote(const std::vector& itens); + +private: + concurrency::ThreadPool pool_; + StorageEstimator estimador_; +}; + +} // namespace retroforge::estimation +\end{CaixaCodigo} + +\begin{CaixaCodigo} +// Arquivo: core/src/estimation/BatchStorageEstimator.cpp +#include "retroforge/estimation/BatchStorageEstimator.hpp" + +namespace retroforge::estimation { + +BatchStorageEstimator::BatchStorageEstimator(size_t numero_de_threads) : pool_(numero_de_threads) {} + +std::vector +BatchStorageEstimator::estimar_lote(const std::vector& itens) { + std::vector resultados(itens.size()); + std::mutex mutex_resultados; // protege apenas contra falso-compartilhamento de escrita + + for (size_t i = 0; i < itens.size(); ++i) { + pool_.enfileirar([this, i, &itens, &resultados, &mutex_resultados] { + std::error_code erro; + auto tamanho = std::filesystem::file_size(itens[i].caminho, erro); + + if (erro) { + // Arquivo removido/inacessivel durante a varredura: entrada + // honesta com tamanho zero, nunca uma excecao que + // derrubaria a estimativa do lote inteiro (mesmo espirito + // do try/catch da Fase 3 do ConsoleIdentifier, Modulo 2). + std::lock_guard trava(mutex_resultados); + resultados[i] = EstimativaArmazenamento{0, 0}; + return; + } + + auto estimativa = estimador_.estimar(itens[i].console, tamanho); + + std::lock_guard trava(mutex_resultados); + resultados[i] = estimativa; + }); + } + + pool_.aguardar_ociosidade(); + return resultados; +} + +} // namespace retroforge::estimation +\end{CaixaCodigo} + +\begin{CaixaInfo} +Cada tarefa escreve em um índice \textbf{distinto} de \texttt{resultados} (\texttt{resultados[i]}), então o \texttt{mutex\_resultados} aqui não protege contra uma verdadeira condição de corrida de dados -- protege contra o caso mais sutil de \textit{false sharing} e generaliza o padrão para qualquer estrutura de resultado futura que não seja tão trivialmente particionável quanto um \texttt{std::vector} pré-dimensionado. Um leitor atento notará que, tecnicamente, escrever em índices diferentes de um \texttt{std::vector} já pré-dimensionado é seguro sem mutex algum -- mantemos a trava aqui por disciplina defensiva e para deixar o padrão pronto para reutilização em cenários menos triviais (Exercício 5.6 discute isso). +\end{CaixaInfo} + +\subsection{Hashing eficiente: \texttt{IHashCalculator} e \texttt{XxHashCalculator}} + +O Scraper (Seção 3.6) só existe se o usuário pedir. Mas quando pedir, precisa de uma hash do arquivo \textbf{inteiro} -- e o Módulo 0 já deixou pronta a técnica certa para isso (leitura em \textit{chunks} de 4 MB, Seção de manipulação binária). Falta encapsular essa técnica atrás de uma abstração, para que o algoritmo de hash em si (\texttt{xxHash} hoje, talvez outro no futuro) não vaze para quem consome o resultado. + +\begin{itemize} + \item \textbf{Por que \texttt{IHashCalculator} existe:} contrato mínimo (Segregação de Interface) para ``calcular uma hash de um arquivo inteiro, em blocos''. Existe separada de qualquer implementação concreta porque o algoritmo de hash é um detalhe de mecanismo -- exatamente o mesmo raciocínio que levou o Módulo 3 a unificar \texttt{ICompressionStrategy} para subprocesso \textbf{e} biblioteca estática. + \item \textbf{Como se comunica com outras classes:} implementada por \texttt{XxHashCalculator} (usa a biblioteca \texttt{xxHash}, prevista desde o Módulo 1 como candidata a \texttt{FetchContent}); consumida por \texttt{MetadataScraperClient} e por \texttt{BatchHashScanner} (Seção~\ref{sec:batch-hash}). + \item \textbf{Onde ela mora:} \texttt{core/include/retroforge/scraping/IHashCalculator.hpp} -- uma segunda pasta nova, \texttt{scraping/}, irmã de \texttt{estimation/}. +\end{itemize} + +\begin{CaixaCodigo} +// Arquivo: core/include/retroforge/scraping/IHashCalculator.hpp +#pragma once + +#include +#include +#include + +namespace retroforge::scraping { + +// Responsabilidade unica: calcular uma hash de um arquivo inteiro, lendo-o +// em blocos (retomando a tecnica de chunks do Modulo 0) em vez de carregar +// o arquivo inteiro na RAM. NAO decide o que fazer com a hash depois -- +// isso e' responsabilidade de MetadataScraperClient. +class IHashCalculator { +public: + virtual ~IHashCalculator() = default; + + // Retorna a hash como string hexadecimal. Lanca std::runtime_error + // se o arquivo nao puder ser aberto. + virtual std::string calcular(const std::filesystem::path& caminho) const = 0; +}; + +} // namespace retroforge::scraping +\end{CaixaCodigo} + +\begin{CaixaCodigo} +// Arquivo: core/include/retroforge/scraping/XxHashCalculator.hpp +#pragma once + +#include "retroforge/scraping/IHashCalculator.hpp" + +namespace retroforge::scraping { + +// Implementacao concreta usando a biblioteca xxHash (extrema velocidade +// de leitura de chunks, ver documentacao Secao 2). Le o arquivo em blocos +// de 4 MB -- o MESMO tamanho de chunk usado na demonstracao do Modulo 0 -- +// para nunca alocar um buffer do tamanho do arquivo inteiro. +class XxHashCalculator : public IHashCalculator { +public: + std::string calcular(const std::filesystem::path& caminho) const override; + +private: + static constexpr size_t TAMANHO_CHUNK = 4 * 1024 * 1024; // 4 MB +}; + +} // namespace retroforge::scraping +\end{CaixaCodigo} + +\begin{CaixaCodigo} +// Arquivo: core/src/scraping/XxHashCalculator.cpp +#include "retroforge/scraping/XxHashCalculator.hpp" + +#include +#include +#include +#include +#include + +#include // API C da biblioteca xxHash + +namespace retroforge::scraping { + +std::string XxHashCalculator::calcular(const std::filesystem::path& caminho) const { + std::ifstream arquivo(caminho, std::ios::in | std::ios::binary); + if (!arquivo.is_open()) { + throw std::runtime_error("Nao foi possivel abrir para hashing: " + caminho.string()); + } + + // XXH64_createState/freeState seguem o mesmo espirito RAII que + // ExternalProcessHandle (Modulo 0) -- aqui usamos um estado de pilha + // simples porque a API xxHash ja oferece reset() sem alocacao extra. + XXH64_state_t estado; + XXH64_reset(&estado, 0); + + std::vector buffer(TAMANHO_CHUNK); + + while (arquivo) { + arquivo.read(buffer.data(), static_cast(buffer.size())); + std::streamsize lidos = arquivo.gcount(); + + if (lidos <= 0) { + break; + } + + XXH64_update(&estado, buffer.data(), static_cast(lidos)); + } + + XXH64_hash_t resultado = XXH64_digest(&estado); + + std::ostringstream saida; + saida << std::hex << std::setw(16) << std::setfill('0') << resultado; + return saida.str(); +} + +} // namespace retroforge::scraping +\end{CaixaCodigo} + +\begin{CaixaAlerta} +Note que \texttt{XxHashCalculator} nunca lê o arquivo inteiro para a memória de uma vez -- exatamente a mesma disciplina do Módulo 0 (\texttt{processar\_arquivo\_em\_chunks}), agora aplicada dentro de uma classe de verdade em vez de uma função livre de demonstração. Isso importa ainda mais aqui do que na identificação (Módulo 2): a identificação lê só os primeiros 64 KB, mas o hashing do Scraper precisa, por definição, ler o arquivo \textbf{inteiro} -- um jogo de PS2 de 4,7 GB sem essa disciplina de \textit{chunks} derrubaria a máquina por falta de memória, especialmente se vários hashes rodarem em paralelo (Seção~\ref{sec:batch-hash}). +\end{CaixaAlerta} + +\subsection{O cliente do Scraper: \texttt{MetadataScraperClient}} + +Com a hash calculada, falta consumir a API pública (Seção 3.6: ``ex: RetroAchievements'') e decidir o que fazer com a resposta -- renomear se o hash for reconhecido, ou avisar e manter o nome original se for um hack/tradução (US02). + +\begin{itemize} + \item \textbf{Por que \texttt{MetadataScraperClient} existe:} orquestra o fluxo completo do Scraper -- calcular a hash (via \texttt{IHashCalculator}, Inversão de Dependência), enviá-la à API, e traduzir a resposta JSON em um resultado tipado que a camada de apresentação (\texttt{cli/}, Módulo 11) pode exibir. Não sabe nada sobre identificação de console nem sobre compressão -- é, de propósito, tão isolado do resto do \texttt{core} quanto \texttt{CompressionRouter} é isolado de \texttt{ConsoleIdentifier} (Módulo 3). + \item \textbf{Como se comunica com outras classes:} depende de \texttt{IHashCalculator} (abstração) e de \texttt{libcurl} internamente (a única classe do projeto, até aqui, que fala com a rede). É consumida exclusivamente pela camada de apresentação, e apenas quando o usuário aciona explicitamente ``Renomear / Buscar Metadados'' (Seção 3.6) -- nunca automaticamente durante uma varredura ou conversão. + \item \textbf{Onde ela mora:} \texttt{core/include/retroforge/scraping/MetadataScraperClient.hpp} e seu \texttt{.cpp} correspondente, ao lado de \texttt{IHashCalculator.hpp}. +\end{itemize} + +\begin{CaixaCodigo} +// Arquivo: core/include/retroforge/scraping/MetadataScraperClient.hpp +#pragma once + +#include +#include +#include + +#include "retroforge/scraping/IHashCalculator.hpp" + +namespace retroforge::scraping { + +// Resultado do scraping: distingue "hash reconhecida" de "hash +// desconhecida" -- a mesma disciplina de resultados tipados que o +// Modulo 3 usou com ResultadoRoteamento, em vez de sobrecarregar um +// simples bool ou uma string vazia com multiplos significados. +struct ResultadoScraping { + bool hash_reconhecida = false; + std::optional nome_oficial; // ex: "God of War - Ghost of Sparta (USA)" +}; + +// Orquestra: calcular hash -> consultar API publica -> traduzir resposta. +// So' e' acionada sob demanda (Secao 3.6: "Apenas se o usuario clicar em +// Renomear / Buscar Metadados") -- NUNCA automaticamente durante uma +// varredura ou conversao em lote (Modulo 4). +class MetadataScraperClient { +public: + explicit MetadataScraperClient(std::unique_ptr calculadora_de_hash); + + // Retorna nullopt se a requisicao de rede falhar (sem internet, + // API fora do ar) -- distinto de "hash desconhecida", que e' uma + // resposta valida da API dizendo "nunca vi este arquivo". + std::optional buscar_metadados(const std::filesystem::path& caminho); + +private: + std::unique_ptr calculadora_de_hash_; +}; + +} // namespace retroforge::scraping +\end{CaixaCodigo} + +\begin{CaixaCodigo} +// Arquivo: core/src/scraping/MetadataScraperClient.cpp +#include "retroforge/scraping/MetadataScraperClient.hpp" + +#include + +#include + +namespace retroforge::scraping { + +namespace { + +// Callback de escrita exigido pela API C do libcurl -- acumula o corpo +// da resposta HTTP em uma std::string, no mesmo espirito de RAII que +// ExternalProcessHandle (Modulo 0) aplica a popen/pclose: o handle CURL* +// e' fechado via curl_easy_cleanup logo abaixo, sempre, mesmo em erro. +size_t escrever_resposta(void* dados, size_t tamanho, size_t nmemb, void* buffer_saida) { + size_t total = tamanho * nmemb; + static_cast(buffer_saida)->append(static_cast(dados), total); + return total; +} + +} // namespace + +MetadataScraperClient::MetadataScraperClient(std::unique_ptr calculadora_de_hash) + : calculadora_de_hash_(std::move(calculadora_de_hash)) {} + +std::optional +MetadataScraperClient::buscar_metadados(const std::filesystem::path& caminho) { + std::string hash; + try { + hash = calculadora_de_hash_->calcular(caminho); + } catch (const std::exception& erro) { + std::cerr << "Falha ao calcular hash para scraping: " << erro.what() << "\n"; + return std::nullopt; + } + + CURL* handle = curl_easy_init(); + if (!handle) { + return std::nullopt; + } + + std::string url = "https://api.retroachievements.exemplo/hash/" + hash; + std::string corpo_resposta; + + curl_easy_setopt(handle, CURLOPT_URL, url.c_str()); + curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, escrever_resposta); + curl_easy_setopt(handle, CURLOPT_WRITEDATA, &corpo_resposta); + curl_easy_setopt(handle, CURLOPT_TIMEOUT, 10L); + + CURLcode codigo = curl_easy_perform(handle); + curl_easy_cleanup(handle); // RAII manual -- sempre chamado, sucesso ou falha + + if (codigo != CURLE_OK) { + return std::nullopt; // sem internet, API fora do ar, timeout, etc. + } + + // NOTA: parsing real de JSON (extrair "nome_padronizado" do corpo) + // e' deixado como Exercicio 5.3 -- o foco deste modulo e' a + // arquitetura do fluxo hash -> rede -> resultado tipado, nao a + // escolha de uma biblioteca JSON especifica. + ResultadoScraping resultado; + resultado.hash_reconhecida = !corpo_resposta.empty(); + if (resultado.hash_reconhecida) { + resultado.nome_oficial = corpo_resposta; // placeholder ate o parsing real + } + + return resultado; +} + +} // namespace retroforge::scraping +\end{CaixaCodigo} + +\begin{CaixaInfo} +Repare a distinção de três níveis em \texttt{std::optional}: (1) \texttt{std::nullopt} -- falha de \textbf{rede} (sem internet, API fora do ar); (2) \texttt{ResultadoScraping\{false, nullopt\}} -- a API respondeu, mas \textbf{não reconhece} esta hash (romhack ou tradução, US02: ``o app avisa que a hash é desconhecida e mantém o nome original''); (3) \texttt{ResultadoScraping\{true, "God of War..."\}} -- \textit{clean dump} reconhecido. Três situações semanticamente diferentes, assim como o Módulo 3 distinguiu \texttt{ConversaoIniciada}/\texttt{BloqueadoPorSeguranca}/\texttt{ConsoleNaoSuportado} em vez de um simples \texttt{bool}. +\end{CaixaInfo} + +\begin{CaixaAlerta} +\texttt{\#include } e \texttt{\#include } só compilam se essas bibliotecas estiverem disponíveis para o compilador -- decisão de \textbf{build} ainda pendente, no mesmo espírito do \texttt{libzip} do Módulo 3. O Módulo 1 já havia reservado o terreno: \texttt{xxHash} via \texttt{FetchContent} (biblioteca pequena, com um arquivo \texttt{.c} de implementação) e \texttt{libcurl} via \texttt{find\_package(CURL REQUIRED)} (biblioteca grande, com dependências do próprio sistema operacional). Formalizar essas duas linhas em \texttt{core/CMakeLists.txt} é trabalho de infraestrutura de build, fora do escopo deste módulo -- mas o código acima já pressupõe que elas vão existir antes de o projeto compilar de verdade. +\end{CaixaAlerta} + +\subsection{Hashing em lote: reaproveitando \texttt{ThreadPool} de novo} +\label{sec:batch-hash} + +A Seção 3.6 da documentação também prevê, implicitamente, que um usuário pode querer buscar metadados para \textbf{vários} arquivos de uma vez (a mesma pasta de 50 jogos da US03). Assim como \texttt{BatchStorageEstimator} (Seção~\ref{sec:batch-estimator}) reaproveitou \texttt{concurrency::ThreadPool} para paralelizar cálculos de tamanho, \texttt{BatchHashScanner} reaproveita a mesma classe para paralelizar hashing -- exatamente a previsão feita ao final do Módulo 4 (``o Módulo 5 vai... reaproveitar a \texttt{concurrency::ThreadPool} deste módulo para paralelizar o hashing de múltiplos arquivos ao mesmo tempo''). + +\begin{itemize} + \item \textbf{Por que \texttt{BatchHashScanner} existe:} aplica \texttt{IHashCalculator} sobre uma lista de caminhos, em paralelo, devolvendo um mapa de caminho para hash. Não decide o que fazer com as hashes -- isso é papel de quem chama, tipicamente alimentando \texttt{MetadataScraperClient} um a um, ou uma futura otimização (Exercício 5.5) que envia lotes de hashes numa única requisição de rede. + \item \textbf{Como se comunica com outras classes:} depende de \texttt{scraping::IHashCalculator} (abstração -- nunca \texttt{XxHashCalculator} diretamente) e de \texttt{concurrency::ThreadPool}, a \textbf{terceira} classe consecutiva neste módulo a reaproveitar a Thread Pool do Módulo 4 sem alterá-la. + \item \textbf{Onde ela mora:} \texttt{core/include/retroforge/scraping/BatchHashScanner.hpp} e seu \texttt{.cpp} correspondente. +\end{itemize} + +\begin{CaixaCodigo} +// Arquivo: core/include/retroforge/scraping/BatchHashScanner.hpp +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "retroforge/concurrency/ThreadPool.hpp" +#include "retroforge/scraping/IHashCalculator.hpp" + +namespace retroforge::scraping { + +class BatchHashScanner { +public: + BatchHashScanner(std::unique_ptr calculadora_de_hash, + size_t numero_de_threads); + + // Hasheia todos os caminhos em paralelo. Arquivos que falharem ao + // abrir simplesmente NAO aparecem no mapa de resultado -- ausencia + // de chave, nao uma string vazia, sinaliza falha (o mesmo padrao de + // "ausencia como sinal" usado pelo unordered_map de estrategias do + // CompressionRouter, Modulo 3). + std::unordered_map + hashear_lote(const std::vector& caminhos); + +private: + std::unique_ptr calculadora_de_hash_; + concurrency::ThreadPool pool_; +}; + +} // namespace retroforge::scraping +\end{CaixaCodigo} + +\begin{CaixaCodigo} +// Arquivo: core/src/scraping/BatchHashScanner.cpp +#include "retroforge/scraping/BatchHashScanner.hpp" + +#include + +namespace retroforge::scraping { + +BatchHashScanner::BatchHashScanner(std::unique_ptr calculadora_de_hash, + size_t numero_de_threads) + : calculadora_de_hash_(std::move(calculadora_de_hash)), pool_(numero_de_threads) {} + +std::unordered_map +BatchHashScanner::hashear_lote(const std::vector& caminhos) { + std::unordered_map resultados; + std::mutex mutex_resultados; // aqui SIM protege contra corrida real -- + // multiplas threads inserindo na MESMA + // unordered_map, diferente do vector + // pre-dimensionado de BatchStorageEstimator. + + for (const auto& caminho : caminhos) { + pool_.enfileirar([this, caminho, &resultados, &mutex_resultados] { + try { + std::string hash = calculadora_de_hash_->calcular(caminho); + + std::lock_guard trava(mutex_resultados); + resultados[caminho.string()] = hash; + } catch (const std::exception& erro) { + std::cerr << "Falha ao hashear " << caminho << ": " << erro.what() << "\n"; + // Arquivo simplesmente ausente do mapa -- fila continua + // (mesmo espirito do try/catch em ChdmanConverter, Modulo 3). + } + }); + } + + pool_.aguardar_ociosidade(); + return resultados; +} + +} // namespace retroforge::scraping +\end{CaixaCodigo} + +\begin{CaixaAlerta} +Diferente de \texttt{BatchStorageEstimator} (onde o \texttt{mutex} era defensivo, sobre índices já disjuntos de um \texttt{vector}), aqui o \texttt{mutex\_resultados} é \textbf{estritamente necessário}: várias threads chamando \texttt{resultados[chave] = valor]} simultaneamente sobre o \textbf{mesmo} \texttt{std::unordered\_map} é uma condição de corrida de dados real -- \texttt{operator[]} pode disparar um \textit{rehash} interno da tabela, invalidando iteradores e ponteiros de outras threads no meio de uma escrita concorrente. Vale comparar as duas classes lado a lado (Exercício 5.6): a mesma \texttt{ThreadPool}, dois padrões de sincronização genuinamente diferentes, dependendo da estrutura de dados de destino. +\end{CaixaAlerta} + +\subsection{Arquitetura de Pastas do Módulo 5} + +Duas pastas novas nascem sob \texttt{core/} neste módulo -- a terceira e quarta vez que isso acontece no livro (\texttt{compression/} no Módulo 3, \texttt{concurrency/}/\texttt{batch/} no Módulo 4): + +\begin{CaixaArvore} +core/ +|-- include/retroforge/ +| |-- io/ [Modulo 0, sem alteracoes] +| |-- process/ [Modulo 0, sem alteracoes] +| |-- identification/ [Modulo 0 e 2, sem alteracoes] +| |-- bootstrap/ [Modulo 2, sem alteracoes] +| |-- models/ [Modulo 0/2, sem alteracoes] +| |-- compression/ [Modulo 3, sem alteracoes] +| |-- concurrency/ [Modulo 4, sem alteracoes -- +| | ThreadPool reaproveitada 3x aqui] +| |-- batch/ [Modulo 4, sem alteracoes] +| |-- estimation/ [NOVO neste modulo] +| | |-- StorageEstimator.hpp [NOVO] +| | `-- BatchStorageEstimator.hpp [NOVO] +| `-- scraping/ [NOVO neste modulo] +| |-- IHashCalculator.hpp [NOVO] +| |-- XxHashCalculator.hpp [NOVO] +| |-- MetadataScraperClient.hpp [NOVO] +| `-- BatchHashScanner.hpp [NOVO] +`-- src/ + |-- estimation/ [NOVO neste modulo] + | |-- StorageEstimator.cpp [NOVO] + | `-- BatchStorageEstimator.cpp [NOVO] + `-- scraping/ [NOVO neste modulo] + |-- XxHashCalculator.cpp [NOVO] + |-- MetadataScraperClient.cpp [NOVO] + `-- BatchHashScanner.cpp [NOVO] +\end{CaixaArvore} + +\begin{CaixaInfo} +Vale notar por que \texttt{estimation/} e \texttt{scraping/} são pastas separadas, e não uma única \texttt{metadata/}: nenhuma classe de \texttt{estimation/} depende de \texttt{scraping/}, nem o contrário -- \texttt{StorageEstimator} nunca precisa de uma hash, e \texttt{MetadataScraperClient} nunca precisa de um multiplicador de compressão. A única coisa que as duas pastas compartilham é o \textbf{reaproveitamento} de \texttt{concurrency::ThreadPool}, e isso já é possível porque a Thread Pool vive em sua própria pasta genérica, sem pertencer a nenhuma das duas. +\end{CaixaInfo} + +\subsection{Duas linhas de CMake pendentes: \texttt{xxHash} e \texttt{libcurl}} + +Seguindo o mesmo hábito de honestidade dos Módulos 3 e 4 (que deixaram registradas as pendências de \texttt{libzip} e \texttt{Threads::Threads}), este módulo introduz duas dependências externas novas que ainda não foram formalizadas em \texttt{core/CMakeLists.txt}. + +\begin{CaixaAlerta} +Sem essas duas adições, nenhum arquivo de \texttt{scraping/} compila. A correção, quando \texttt{core/CMakeLists.txt} for de fato atualizado (fora do escopo deste módulo): +\begin{CaixaCodigo} +# Adicao futura ao core/CMakeLists.txt + +include(FetchContent) +FetchContent_Declare( + xxhash + GIT_REPOSITORY https://github.com/Cyan4973/xxHash.git + GIT_TAG v0.8.2 + SOURCE_SUBDIR cmake_unofficial +) +FetchContent_MakeAvailable(xxhash) + +find_package(CURL REQUIRED) + +target_link_libraries(retroforge_core PUBLIC + xxhash + CURL::libcurl +) +\end{CaixaCodigo} +\texttt{xxHash} segue a estratégia \texttt{FetchContent} já anunciada no Módulo 1 (biblioteca pequena, compilada junto); \texttt{libcurl} segue \texttt{find\_package}, exatamente como o Módulo 1 previu (biblioteca grande, com dependências do próprio sistema operacional, localizada no ambiente do usuário ou do runner de CI). +\end{CaixaAlerta} + +\subsection{Onde o projeto está agora, de fato} + +Como nos módulos anteriores: no repositório \texttt{Dev-Sams1012/RetroForge}, nenhuma das pastas \texttt{estimation/} ou \texttt{scraping/} existe ainda, e nenhuma das duas linhas de CMake acima foi commitada. Este módulo entrega mais uma peça do roteiro incremental; o próximo Pull Request real precisa adicionar as duas pastas, suas classes, e as dependências de build necessárias, mantendo o CI (Módulo 1) verde. + +\subsection{Exercícios Práticos do Módulo 5} + +\begin{CaixaDesafio} +Lembrete das etiquetas: \textbf{[projeto]} = vira arquivo/pasta permanente do repositório; \textbf{[laboratório]} = validação prática descartável (não precisa sobreviver no repositório depois de feito). + +\begin{enumerate}[label=\textbf{5.\arabic*}] + \item \textbf{[projeto]} Implemente \texttt{StorageEstimator} exatamente como apresentado. Escreva um \texttt{main()} temporário que chame \texttt{estimar()} para \texttt{Console::PS2} com um tamanho fictício de 4,7 GB e confirme que o resultado bate com a fração esperada (0.60). Confirme também que \texttt{Console::DreamcastModificado} devolve o mesmo tamanho original -- nenhuma economia prometida. + \item \textbf{[laboratório]} Implemente \texttt{BatchStorageEstimator} e monte uma lista de 20 \texttt{ItemParaEstimativa} fictícios (apontando para arquivos que precisam existir de verdade, já que \texttt{std::filesystem::file\_size} exige isso -- use \texttt{std::ofstream} para criá-los com tamanhos variados). Confirme que \texttt{estimar\_lote} devolve 20 resultados, na mesma ordem da entrada. Este \texttt{main()} é descartável. + \item \textbf{[projeto]} Implemente \texttt{XxHashCalculator} e confirme, com um teste manual, que hashear o mesmo arquivo duas vezes produz sempre a mesma string hexadecimal, e que dois arquivos com conteúdos diferentes produzem hashes diferentes. Depois, implemente o parsing real de JSON dentro de \texttt{MetadataScraperClient::buscar\_metadados} (deixado como \textit{placeholder} no corpo deste módulo) usando uma biblioteca de sua escolha. + \item \textbf{[laboratório]} Sem acesso real à API: crie um teste manual que simule as três respostas possíveis de \texttt{MetadataScraperClient::buscar\_metadados} (\texttt{std::nullopt}, hash desconhecida, hash reconhecida) e confirme que a camada que consome o resultado trata as três situações de forma visivelmente diferente -- por exemplo, imprimindo mensagens distintas para cada caso. + \item \textbf{[laboratório]} Discuta por escrito (não precisa codificar): o \texttt{BatchHashScanner} atual faz uma requisição de rede por arquivo, dentro de \texttt{MetadataScraperClient}, se você encadeá-los. Para uma pasta de 50 jogos, isso significa até 50 requisições HTTP separadas. Como você adaptaria o fluxo para enviar as 50 hashes calculadas em uma \textbf{única} requisição em lote, supondo que a API de metadados suportasse isso? Que classe nova (ou método adicional) seria necessário, e ela pertenceria a \texttt{scraping/} ou a uma pasta diferente? + \item \textbf{[laboratório]} Compare, com um pequeno teste manual, o comportamento de \texttt{BatchStorageEstimator} (mutex defensivo sobre um \texttt{vector} pré-dimensionado) com o de \texttt{BatchHashScanner} (mutex estritamente necessário sobre um \texttt{unordered\_map}). Remova o mutex de \texttt{BatchStorageEstimator} e confirme, com um \textit{sanitizer} de threads (\texttt{-fsanitize=thread}, se disponível no seu compilador) ou apenas por análise, que nenhuma condição de corrida real acontece. Depois, tente remover o mutex de \texttt{BatchHashScanner} e explique por escrito por que isso seria perigoso. +\end{enumerate} +\end{CaixaDesafio} + +\begin{CaixaResumo} +Neste módulo você aprendeu: +\begin{itemize} + \item Por que estimativa de armazenamento (barata, sempre executada) e scraping de metadados (caro, sob demanda) merecem pastas e classes completamente separadas, apesar de ambas lidarem com ``metadados'' em um sentido amplo -- a mesma disciplina de Responsabilidade Única aplicada, desta vez, à granularidade de \textbf{pastas} inteiras, não só de classes. + \item Como um \texttt{switch} explícito sobre \texttt{Console} em \texttt{StorageEstimator} reaproveita o mesmo hábito de segurança do Módulo 2 (\texttt{-Wswitch} forçando tratamento de todo valor do enum), mesmo em uma classe que nunca decide rotas de compressão de verdade. + \item Que a separação entre \texttt{concurrency::ThreadPool} (genérica) e \texttt{batch::BatchConversionManager} (específica de domínio), estabelecida no Módulo 4, permitiu que este módulo reaproveitasse a Thread Pool \textbf{três vezes} -- em \texttt{BatchStorageEstimator} e \texttt{BatchHashScanner} -- sem que nenhuma dessas classes precisasse herdar uma dependência de \texttt{compression::CompressionRouter} que não fazia sentido para elas. + \item Como a técnica de leitura em \textit{chunks} do Módulo 0 (nunca alocar um buffer do tamanho do arquivo inteiro) se torna indispensável, e não apenas ilustrativa, no momento em que o Scraper precisa hashear um arquivo de 4,7 GB de verdade. + \item Por que \texttt{std::optional} com um campo booleano interno distingue três situações semanticamente diferentes (falha de rede, hash desconhecida, hash reconhecida) -- o mesmo padrão de resultados tipados que o Módulo 3 usou com \texttt{ResultadoRoteamento}, em vez de sobrecarregar um \texttt{bool} ou uma string vazia. + \item Que nem todo uso de mutex dentro de uma \texttt{ThreadPool} protege o mesmo tipo de risco: escrita em índices disjuntos de um \texttt{vector} pré-dimensionado (\texttt{BatchStorageEstimator}) é um risco mais teórico do que escrita concorrente no mesmo \texttt{unordered\_map} (\texttt{BatchHashScanner}), e reconhecer essa diferença evita tanto sincronização insuficiente quanto sincronização desnecessária. + \item Que duas novas dependências externas (\texttt{xxHash} via \texttt{FetchContent}, \texttt{libcurl} via \texttt{find\_package}) seguem exatamente as duas estratégias que o Módulo 1 já havia anunciado, sem exigir nenhuma decisão nova de infraestrutura de build. +\end{itemize} + +Com o console identificado (Módulo 2), roteado com segurança (Módulo 3), processado em paralelo (Módulo 4), e agora estimado e opcionalmente enriquecido com metadados online (este módulo), o RetroForge já cobre o ciclo de vida completo de um único arquivo ou de um HD inteiro -- exceto por um detalhe que todos os módulos anteriores assumiram como resolvido sem nunca formalizá-lo: \textbf{como, exatamente, uma pasta se transforma na lista de arquivos que alimenta tudo isso}. O Módulo 6 vai se afastar temporariamente da regra de negócio para tratar de integração com o sistema operacional e versionamento -- e o Módulo 7, mais adiante, vai finalmente formalizar a varredura recursiva de diretórios (\texttt{std::filesystem::recursive\_directory\_iterator}) que \texttt{BatchStorageEstimator} e \texttt{BatchHashScanner} já pressupõem receber pronta. +\end{CaixaResumo}