diff --git a/.ci/ubuntu_install.sh b/.ci/ubuntu_install.sh new file mode 100755 index 0000000..e596a01 --- /dev/null +++ b/.ci/ubuntu_install.sh @@ -0,0 +1,3 @@ +#!/bin/bash +apt-get update -qq +apt-get install cmake git gcc g++ make ninja-build -qq diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml new file mode 100644 index 0000000..9bb3935 --- /dev/null +++ b/.github/workflows/ubuntu.yml @@ -0,0 +1,24 @@ +name: Ubuntu + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +env: + BUILD_TYPE: Release + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + submodules: recursive + - name: Install packages + run: sudo .ci/ubuntu_install.sh + - name: Configure CMake + run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -GNinja + - name: Build + run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} diff --git a/CMakeLists.txt b/CMakeLists.txt index ab1fb75..7ba6196 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,14 +6,20 @@ set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) if (WIN32) - set(LIBRAGEPHOTO_NAME libragephoto) - set(LIBRAGEPHOTO_DEFINES USE_WINAPI) + list(APPEND LIBRAGEPHOTO_DEFINES + USE_WINAPI + ) +endif() + +message("-- Testing iconv") +try_run(ICONV_RUN ICONV_COMPILE ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/tests/IconvTest.cpp) +if (ICONV_COMPILE) + list(APPEND LIBRAGEPHOTO_DEFINES + USE_ICONV + ) + message("-- Testing iconv - yes") else() - set(LIBRAGEPHOTO_NAME ragephoto) - try_run(ICONV_RUN ICONV_COMPILE ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/tests/IconvTest.cpp) - if (ICONV_COMPILE) - set(LIBRAGEPHOTO_DEFINES USE_ICONV) - endif() + message("-- Testing iconv - no") endif() option(WITH_BENCHMARK "Benchmark RagePhoto Parsing Engine" OFF) @@ -23,17 +29,17 @@ if (WITH_BENCHMARK) ) endif() -project(${LIBRAGEPHOTO_NAME} LANGUAGES CXX) -add_library(${LIBRAGEPHOTO_NAME} SHARED +project(ragephoto LANGUAGES CXX) +add_library(ragephoto SHARED src/libragephoto_global.h src/RagePhoto.cpp src/RagePhoto.h ) -target_compile_definitions(${LIBRAGEPHOTO_NAME} PRIVATE +target_compile_definitions(ragephoto PRIVATE LIBRAGEPHOTO_LIBRARY ${LIBRAGEPHOTO_DEFINES} ) -install(TARGETS ${LIBRAGEPHOTO_NAME} DESTINATION lib) +install(TARGETS ragephoto DESTINATION lib) install(FILES src/RagePhoto.h src/libragephoto_global.h @@ -47,5 +53,5 @@ add_executable(ragephoto-extract src/RagePhoto-Extract.cpp src/RagePhoto.h ) -target_link_libraries(ragephoto-extract ${LIBRAGEPHOTO_NAME}) +target_link_libraries(ragephoto-extract ragephoto) install(TARGETS ragephoto-extract DESTINATION bin) diff --git a/README.md b/README.md new file mode 100644 index 0000000..6abfa6e --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +## libragephoto +Open Source RAGE Photo Parser for GTA V + +- Read RAGE Photos error free and correct +- Support for metadata stored in RAGE Photos +- Export RAGE Photos to jpeg with ragephoto-extract +- High Efficient and Simple C++ API + +#### Build libragephoto & ragephoto-extract + +```bash +git clone https://gitlab.com/Syping/libragephoto && cd libragephoto +mkdir -p build && cd build +cmake ../ +make -j $(nproc --all) +sudo make install +``` + +#### How to Use libragephoto + +```cpp +RagePhoto ragePhoto; +bool loaded = ragePhoto.load(data, size); +const char* photoData = ragePhoto.photoData(); +uint32_t photoSize = ragePhoto.photoSize(); +std::string json = ragePhoto.json(); +std::string title = ragePhoto.title(); +RagePhoto::Error error = ragePhoto.error(); +``` + +#### How to Use ragephoto-extract + +```bash +ragephoto-extract PGTA5123456789 photo.jpg +``` diff --git a/src/RagePhoto-Extract.cpp b/src/RagePhoto-Extract.cpp index 79b9f00..260fec3 100644 --- a/src/RagePhoto-Extract.cpp +++ b/src/RagePhoto-Extract.cpp @@ -22,48 +22,81 @@ int main(int argc, char *argv[]) { if (argc != 3) { - std::cout << "Usage: " << argv[0] << " [snapmatic] [jpegout]" << std::endl; + std::cout << "Usage: " << argv[0] << " [photo] [jpegout]" << std::endl; return 0; } - // Initialise RagePhoto - RagePhoto ragePhoto; + // Make it crash when RagePhoto have a bug which make the deinitialisation to fail + { + // Initialise RagePhoto + RagePhoto ragePhoto; - // Read file - FILE *file = fopen(argv[1], "rb"); - if (!file) - return -1; - const int fseek_end_value = fseek(file, 0, SEEK_END); - if (fseek_end_value == -1) { + // Read file + FILE *file = fopen(argv[1], "rb"); + if (!file) { + std::cout << "Failed to open import file" << std::endl; + return -1; + } + const int fseek_end_value = fseek(file, 0, SEEK_END); + if (fseek_end_value == -1) { + fclose(file); + std::cout << "Failed to read file" << std::endl; + return -1; + } + const size_t file_size = ftell(file); + if (file_size == -1) { + fclose(file); + std::cout << "Failed to read file" << std::endl; + return -1; + } + const int fseek_set_value = fseek(file, 0, SEEK_SET); + if (fseek_set_value == -1) { + fclose(file); + std::cout << "Failed to read file" << std::endl; + return -1; + } + char *data = static_cast(malloc(file_size)); + const size_t file_rsize = fread(data, 1, file_size, file); + if (file_size != file_rsize) { + fclose(file); + std::cout << "Failed to read file" << std::endl; + return -1; + } fclose(file); - return -1; - } - size_t file_size = ftell(file); - if (file_size == -1) { - fclose(file); - return -1; - } - const int fseek_set_value = fseek(file, 0, SEEK_SET); - if (fseek_set_value == -1) { - fclose(file); - return -1; - } - char *data = static_cast(malloc(file_size)); - const size_t file_rsize = fread(data, 1, file_size, file); - if (file_size != file_rsize) { - fclose(file); - return -1; - } - ragePhoto.load(data, file_size); - fclose(file); + // Load Photo + const bool loaded = ragePhoto.load(data, file_size); - // Write jpeg - file = fopen(argv[2], "wb"); - if (!file) - return -1; - fwrite(ragePhoto.photoData(), sizeof(char), ragePhoto.photoSize(), file); - fclose(file); + // Deinitialise data after Photo loaded + free(data); + + if (!loaded) { + const RagePhoto::Error error = ragePhoto.error(); + if (error <= RagePhoto::Error::PhotoReadError) { + std::cout << "Failed to load photo" << std::endl; + return 1; + } + } + + // Write jpeg + file = fopen(argv[2], "wb"); + if (!file) { + std::cout << "Failed to open export file" << std::endl; + return -1; + } + const size_t written = fwrite(ragePhoto.photoData(), sizeof(char), ragePhoto.photoSize(), file); + fclose(file); + + if (written != ragePhoto.photoSize()) { + std::cout << "Failed to write file" << std::endl; + return -1; + } + + std::cout << "Photo successfully exported" << std::endl; + + // Clear RagePhoto (provocate crash when pointer leak) + ragePhoto.clear(); + } return 0; } diff --git a/src/RagePhoto.cpp b/src/RagePhoto.cpp index 3fba132..2624479 100644 --- a/src/RagePhoto.cpp +++ b/src/RagePhoto.cpp @@ -31,7 +31,9 @@ RagePhoto::RagePhoto() { + p_photoLoaded = false; p_photoData = nullptr; + p_error = Error::Uninitialised; } RagePhoto::~RagePhoto() @@ -39,29 +41,52 @@ RagePhoto::~RagePhoto() free(p_photoData); } +void RagePhoto::clear() +{ + if (p_photoLoaded) { + free(p_photoData); + p_photoData = nullptr; + p_photoLoaded = false; + } + p_descriptionString.clear(); + p_jsonString.clear(); + p_photoString.clear(); + p_titleString.clear(); + p_error = Error::Uninitialised; +} + bool RagePhoto::load(const char *data, size_t length) { #ifdef RAGEPHOTO_BENCHMARK auto benchmark_parse_start = std::chrono::high_resolution_clock::now(); #endif + // Avoid data conflicts + clear(); + size_t pos = 0; char uInt32Buffer[4]; - size_t size = bRead(data, uInt32Buffer, &pos, 4, length); - if (size != 4) + size_t size = readBuffer(data, uInt32Buffer, &pos, 4, length); + if (size != 4) { + p_error = Error::NoFormatIdentifier; // 1 return false; + } uint32_t format = charToUInt32LE(uInt32Buffer); if (format == static_cast(PhotoFormat::GTA5)) { char photoHeader[256]; - size = bRead(data, photoHeader, &pos, 256, length); - if (size != 256) + size = readBuffer(data, photoHeader, &pos, 256, length); + if (size != 256) { + p_error = Error::IncompleteHeader; // 3 return false; + } #ifdef USE_ICONV iconv_t iconv_in = iconv_open("UTF-8", "UTF-16LE"); - if (iconv_in == (iconv_t)-1) + if (iconv_in == (iconv_t)-1) { + p_error = Error::UnicodeInitError; // 4 return false; + } char photoString[256]; size_t src_s = sizeof(photoHeader); size_t dst_s = sizeof(photoString); @@ -69,138 +94,193 @@ bool RagePhoto::load(const char *data, size_t length) char *dst = photoString; size_t ret = iconv(iconv_in, &src, &src_s, &dst, &dst_s); iconv_close(iconv_in); - if (ret == static_cast(-1)) + if (ret == static_cast(-1)) { + p_error = Error::UnicodeHeaderError; // 5 return false; + } p_photoString = std::string(photoString); #endif - size = bRead(data, uInt32Buffer, &pos, 4, length); - if (size != 4) + size = readBuffer(data, uInt32Buffer, &pos, 4, length); + if (size != 4) { + p_error = Error::IncompleteChecksum; // 6 return false; + } p_headerSum = charToUInt32LE(uInt32Buffer); - size = bRead(data, uInt32Buffer, &pos, 4, length); - if (size != 4) + size = readBuffer(data, uInt32Buffer, &pos, 4, length); + if (size != 4) { + p_error = Error::IncompleteEOF; // 7 return false; + } p_endOfFile = charToUInt32LE(uInt32Buffer); - size = bRead(data, uInt32Buffer, &pos, 4, length); - if (size != 4) + size = readBuffer(data, uInt32Buffer, &pos, 4, length); + if (size != 4) { + p_error = Error::IncompleteJsonOffset; // 8 return false; + } p_jsonOffset = charToUInt32LE(uInt32Buffer); - size = bRead(data, uInt32Buffer, &pos, 4, length); - if (size != 4) + size = readBuffer(data, uInt32Buffer, &pos, 4, length); + if (size != 4) { + p_error = Error::IncompleteTitleOffset; // 9 return false; + } p_titlOffset = charToUInt32LE(uInt32Buffer); - size = bRead(data, uInt32Buffer, &pos, 4, length); - if (size != 4) + size = readBuffer(data, uInt32Buffer, &pos, 4, length); + if (size != 4) { + p_error = Error::IncompleteDescOffset; // 10 return false; + } p_descOffset = charToUInt32LE(uInt32Buffer); char markerBuffer[4]; - size = bRead(data, markerBuffer, &pos, 4, length); - if (size != 4) + size = readBuffer(data, markerBuffer, &pos, 4, length); + if (size != 4) { + p_error = Error::IncompleteJpegMarker; // 11 return false; - if (strncmp(markerBuffer, "JPEG", 4) != 0) + } + if (strncmp(markerBuffer, "JPEG", 4) != 0) { + p_error = Error::IncorrectJpegMarker; // 12 return false; + } - size = bRead(data, uInt32Buffer, &pos, 4, length); - if (size != 4) + size = readBuffer(data, uInt32Buffer, &pos, 4, length); + if (size != 4) { + p_error = Error::IncompletePhotoBuffer; // 13 return false; + } p_photoBuffer = charToUInt32LE(uInt32Buffer); - size = bRead(data, uInt32Buffer, &pos, 4, length); - if (size != 4) + size = readBuffer(data, uInt32Buffer, &pos, 4, length); + if (size != 4) { + p_error = Error::IncompletePhotoSize; // 14 return false; + } p_photoSize = charToUInt32LE(uInt32Buffer); p_photoData = static_cast(malloc(p_photoSize)); if (!p_photoData) { + p_error = Error::PhotoMallocError; // 15 return false; } - size = bRead(data, p_photoData, &pos, p_photoSize, length); + size = readBuffer(data, p_photoData, &pos, p_photoSize, length); if (size != p_photoSize) { free(p_photoData); + p_photoData = nullptr; + p_error = Error::PhotoReadError; // 16 + return false; + } + p_photoLoaded = true; + + pos = p_jsonOffset + 264; + size = readBuffer(data, markerBuffer, &pos, 4, length); + if (size != 4) { + p_error = Error::IncompleteJsonMarker; // 17 + return false; + } + if (strncmp(markerBuffer, "JSON", 4) != 0) { + p_error = Error::IncorrectJsonMarker; // 18 return false; } - pos = p_jsonOffset + 264; - size = bRead(data, markerBuffer, &pos, 4, length); - if (size != 4) - return false; - if (strncmp(markerBuffer, "JSON", 4) != 0) - return false; - - size = bRead(data, uInt32Buffer, &pos, 4, length); - if (size != 4) + size = readBuffer(data, uInt32Buffer, &pos, 4, length); + if (size != 4) { + p_error = Error::IncompleteJsonBuffer; // 19 return false; + } p_jsonBuffer = charToUInt32LE(uInt32Buffer); char *t_jsonData = static_cast(malloc(p_jsonBuffer)); - if (!t_jsonData) + if (!t_jsonData) { + p_error = Error::JsonMallocError; // 20 return false; - size = bRead(data, t_jsonData, &pos, p_jsonBuffer, length); + } + size = readBuffer(data, t_jsonData, &pos, p_jsonBuffer, length); if (size != p_jsonBuffer) { free(t_jsonData); + p_error = Error::JsonReadError; // 21 return false; } p_jsonString = std::string(t_jsonData); free(t_jsonData); pos = p_titlOffset + 264; - size = bRead(data, markerBuffer, &pos, 4, length); - if (size != 4) + size = readBuffer(data, markerBuffer, &pos, 4, length); + if (size != 4) { + p_error = Error::IncompleteTitleMarker; // 22 return false; - if (strncmp(markerBuffer, "TITL", 4) != 0) + } + if (strncmp(markerBuffer, "TITL", 4) != 0) { + p_error = Error::IncorrectTitleMarker; // 23 return false; + } - size = bRead(data, uInt32Buffer, &pos, 4, length); - if (size != 4) + size = readBuffer(data, uInt32Buffer, &pos, 4, length); + if (size != 4) { + p_error = Error::IncompleteTitleBuffer; // 24 return false; + } p_titlBuffer = charToUInt32LE(uInt32Buffer); char *t_titlData = static_cast(malloc(p_titlBuffer)); - if (!t_titlData) + if (!t_titlData) { + p_error = Error::TitleMallocError; // 25 return false; - size = bRead(data, t_titlData, &pos, p_titlBuffer, length); + } + size = readBuffer(data, t_titlData, &pos, p_titlBuffer, length); if (size != p_titlBuffer) { free(t_titlData); + p_error = Error::TitleReadError; // 26 return false; } p_titleString = std::string(t_titlData); free(t_titlData); pos = p_descOffset + 264; - size = bRead(data, markerBuffer, &pos, 4, length); - if (size != 4) + size = readBuffer(data, markerBuffer, &pos, 4, length); + if (size != 4) { + p_error = Error::IncompleteDescMarker; // 27 return false; - if (strncmp(markerBuffer, "DESC", 4) != 0) + } + if (strncmp(markerBuffer, "DESC", 4) != 0) { + p_error = Error::IncorrectDescMarker; // 28 return false; + } - size = bRead(data, uInt32Buffer, &pos, 4, length); - if (size != 4) + size = readBuffer(data, uInt32Buffer, &pos, 4, length); + if (size != 4) { + p_error = Error::IncompleteDescBuffer; // 29 return false; + } p_descBuffer = charToUInt32LE(uInt32Buffer); char *t_descData = static_cast(malloc(p_descBuffer)); - if (!t_descData) + if (!t_descData) { + p_error = Error::DescMallocError; // 30 return false; - size = bRead(data, t_descData, &pos, p_descBuffer, length); + } + size = readBuffer(data, t_descData, &pos, p_descBuffer, length); if (size != p_descBuffer) { free(t_descData); + p_error = Error::DescReadError; // 31 return false; } p_descriptionString = std::string(t_descData); free(t_descData); pos = p_endOfFile + 260; - size = bRead(data, markerBuffer, &pos, 4, length); - if (size != 4) + size = readBuffer(data, markerBuffer, &pos, 4, length); + if (size != 4) { + p_error = Error::IncompleteJendMarker; // 32 return false; - if (strncmp(markerBuffer, "JEND", 4) != 0) + } + if (strncmp(markerBuffer, "JEND", 4) != 0) { + p_error = Error::IncorrectJendMarker; // 33 return false; + } #ifdef RAGEPHOTO_BENCHMARK auto benchmark_parse_end = std::chrono::high_resolution_clock::now(); @@ -208,19 +288,32 @@ bool RagePhoto::load(const char *data, size_t length) std::cout << "Benchmark: " << benchmark_ns.count() << "ns" << std::endl; #endif + p_error = Error::NoError; // 255 return true; } + p_error = Error::IncompatibleFormat; // 2 return false; } +RagePhoto::Error RagePhoto::error() +{ + return p_error; +} + const char* RagePhoto::photoData() { - return p_photoData; + if (p_photoLoaded) + return p_photoData; + else + return nullptr; } const uint32_t RagePhoto::photoSize() { - return p_photoSize; + if (p_photoLoaded) + return p_photoSize; + else + return 0; } const std::string RagePhoto::description() @@ -243,7 +336,7 @@ const std::string RagePhoto::title() return p_titleString; } -size_t RagePhoto::bRead(const char *input, char *output, size_t *pos, size_t len) +size_t RagePhoto::readBuffer(const char *input, char *output, size_t *pos, size_t len) { #ifdef READ_USE_FOR for (size_t i = 0; i < len; i++) { @@ -256,7 +349,7 @@ size_t RagePhoto::bRead(const char *input, char *output, size_t *pos, size_t len return len; } -size_t RagePhoto::bRead(const char *input, char *output, size_t *pos, size_t len, size_t inputLen) +size_t RagePhoto::readBuffer(const char *input, char *output, size_t *pos, size_t len, size_t inputLen) { size_t readLen = 0; if (*pos >= inputLen) diff --git a/src/RagePhoto.h b/src/RagePhoto.h index 8f4a899..51e29ea 100644 --- a/src/RagePhoto.h +++ b/src/RagePhoto.h @@ -27,14 +27,52 @@ class LIBRAGEPHOTO_EXPORT RagePhoto { public: + enum class Error : uint8_t { + DescMallocError = 30, + DescReadError = 31, + IncompatibleFormat = 2, + IncompleteChecksum = 6, + IncompleteDescBuffer = 29, + IncompleteDescMarker = 27, + IncompleteDescOffset = 10, + IncompleteEOF = 7, + IncompleteHeader = 3, + IncompleteJendMarker = 32, + IncompleteJpegMarker = 11, + IncompleteJsonBuffer = 19, + IncompleteJsonMarker = 17, + IncompleteJsonOffset = 8, + IncompletePhotoBuffer = 13, + IncompletePhotoSize = 14, + IncompleteTitleBuffer = 24, + IncompleteTitleMarker = 22, + IncompleteTitleOffset = 9, + IncorrectDescMarker = 28, + IncorrectJendMarker = 33, + IncorrectJpegMarker = 12, + IncorrectJsonMarker = 18, + IncorrectTitleMarker = 23, + JsonMallocError = 20, + JsonReadError = 21, + NoError = 255, + NoFormatIdentifier = 1, + PhotoMallocError = 15, + PhotoReadError = 16, + TitleMallocError = 25, + TitleReadError = 26, + UnicodeHeaderError = 5, + UnicodeInitError = 4, + Uninitialised = 0, + }; enum class PhotoFormat : uint32_t { GTA5 = 0x01000000U, RDR2 = 0x04000000U, - Undefined = 0, }; RagePhoto(); ~RagePhoto(); + void clear(); bool load(const char *data, size_t length); + Error error(); const char *photoData(); const uint32_t photoSize(); const std::string description(); @@ -43,13 +81,15 @@ public: const std::string title(); private: - inline size_t bRead(const char *input, char *output, size_t *pos, size_t len); - inline size_t bRead(const char *input, char *output, size_t *pos, size_t len, size_t inputLen); + inline size_t readBuffer(const char *input, char *output, size_t *pos, size_t len); + inline size_t readBuffer(const char *input, char *output, size_t *pos, size_t len, size_t inputLen); inline uint32_t charToUInt32BE(char *x); inline uint32_t charToUInt32LE(char *x); inline void uInt32ToCharBE(uint32_t x, char *y); inline void uInt32ToCharLE(uint32_t x, char *y); + bool p_photoLoaded; char* p_photoData; + Error p_error; std::string p_descriptionString; std::string p_jsonString; std::string p_photoString;