├── .clang-format ├── .github └── workflows │ └── build-msys2.yml ├── .gitignore ├── .idea ├── .gitignore ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── editor.xml ├── misc.xml ├── modules.xml ├── pmlxzj_unlocker.iml └── vcs.xml ├── CMakeLists.txt ├── LICENSE ├── README.MD ├── assets ├── locked.webp └── unlocked.webp ├── cmake └── mingw-w64-x86_64.cmake ├── cmd_disable_audio.c ├── cmd_extract_audio.c ├── cmd_info.c ├── cmd_remove_watermark.c ├── cmd_unlock_exe.c ├── docs └── exe_player_spec.adoc ├── main.c ├── pmlxzj.c ├── pmlxzj.h ├── pmlxzj_audio.c ├── pmlxzj_audio_aac.h ├── pmlxzj_commands.h ├── pmlxzj_enum_names.h ├── pmlxzj_frame.c ├── pmlxzj_utils.h └── pmlxzj_win32.h /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: Chromium 2 | ColumnLimit: 120 -------------------------------------------------------------------------------- /.github/workflows/build-msys2.yml: -------------------------------------------------------------------------------- 1 | name: Build (msys2) 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: windows-latest 13 | name: "🚧 ${{ matrix.icon }} ${{ matrix.sys }}" 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | include: 18 | - icon: 🟦 19 | sys: mingw32 20 | - icon: 🟨 21 | sys: ucrt64 22 | defaults: 23 | run: 24 | shell: msys2 {0} 25 | steps: 26 | - uses: actions/checkout@v4 27 | 28 | - name: '${{ matrix.icon }} Set up msys2-${{ matrix.sys }}' 29 | uses: msys2/setup-msys2@v2 30 | with: 31 | msystem: "${{matrix.sys}}" 32 | update: true 33 | install: curl git 34 | pacboy: >- 35 | toolchain:p 36 | cmake:p 37 | ninja:p 38 | 39 | - name: Configure & Build 40 | run: | 41 | cmake \ 42 | -DCMAKE_BUILD_TYPE=Release \ 43 | -DSTATIC_ZLIB=ON \ 44 | -B build -S . 45 | cmake --build build \ 46 | --config Release \ 47 | --target pmlxzj_unlocker 48 | 49 | - name: Prepare Archive 50 | run: | 51 | rm -rf dist 52 | mkdir -p dist 53 | cp -R LICENSE README.MD assets build/*.exe dist/ 54 | 55 | - uses: actions/upload-artifact@v4 56 | with: 57 | name: "pmlxzj_unlocker_${{matrix.sys}}" 58 | path: dist/ 59 | bundle: 60 | needs: [ build ] 61 | runs-on: ubuntu-latest 62 | steps: 63 | - uses: actions/download-artifact@v4 64 | with: 65 | name: pmlxzj_unlocker_ucrt64 66 | path: dist_ucrt64 67 | 68 | - uses: actions/download-artifact@v4 69 | with: 70 | name: pmlxzj_unlocker_mingw32 71 | path: dist_mingw32 72 | 73 | - name: bundle all 74 | run: | 75 | mkdir -p dist 76 | for exe_path in dist_mingw32/*.exe; do 77 | mv "$exe_path" "${exe_path%.exe}_i686.exe" 78 | done 79 | # for exe_path in dist_ucrt64/*.exe; do 80 | # mv "$exe_path" "${exe_path%.exe}_ucrt64.exe" 81 | # done 82 | 83 | cp -r dist_mingw32/*.exe dist/. 84 | cp -r dist_ucrt64/. dist/. 85 | 86 | - uses: actions/upload-artifact@v4 87 | with: 88 | name: pmlxzj_unlocker 89 | path: dist/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | sample/ 2 | build/ 3 | *.7z 4 | 5 | ### C template 6 | # Prerequisites 7 | *.d 8 | 9 | # Object files 10 | *.o 11 | *.ko 12 | *.obj 13 | *.elf 14 | 15 | # Linker output 16 | *.ilk 17 | *.map 18 | *.exp 19 | 20 | # Precompiled Headers 21 | *.gch 22 | *.pch 23 | 24 | # Libraries 25 | *.lib 26 | *.a 27 | *.la 28 | *.lo 29 | 30 | # Shared objects (inc. Windows DLLs) 31 | *.dll 32 | *.so 33 | *.so.* 34 | *.dylib 35 | 36 | # Executables 37 | *.exe 38 | *.out 39 | *.app 40 | *.i*86 41 | *.x86_64 42 | *.hex 43 | 44 | # Debug files 45 | *.dSYM/ 46 | *.su 47 | *.idb 48 | *.pdb 49 | 50 | # Kernel Module Compile Results 51 | *.mod* 52 | *.cmd 53 | .tmp_versions/ 54 | modules.order 55 | Module.symvers 56 | Mkfile.old 57 | dkms.conf 58 | 59 | ### CLion template 60 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 61 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 62 | 63 | # User-specific stuff 64 | .idea/**/workspace.xml 65 | .idea/**/tasks.xml 66 | .idea/**/usage.statistics.xml 67 | .idea/**/dictionaries 68 | .idea/**/shelf 69 | 70 | # AWS User-specific 71 | .idea/**/aws.xml 72 | 73 | # Generated files 74 | .idea/**/contentModel.xml 75 | 76 | # Sensitive or high-churn files 77 | .idea/**/dataSources/ 78 | .idea/**/dataSources.ids 79 | .idea/**/dataSources.local.xml 80 | .idea/**/sqlDataSources.xml 81 | .idea/**/dynamic.xml 82 | .idea/**/uiDesigner.xml 83 | .idea/**/dbnavigator.xml 84 | 85 | # Gradle 86 | .idea/**/gradle.xml 87 | .idea/**/libraries 88 | 89 | # Gradle and Maven with auto-import 90 | # When using Gradle or Maven with auto-import, you should exclude module files, 91 | # since they will be recreated, and may cause churn. Uncomment if using 92 | # auto-import. 93 | # .idea/artifacts 94 | # .idea/compiler.xml 95 | # .idea/jarRepositories.xml 96 | # .idea/modules.xml 97 | # .idea/*.iml 98 | # .idea/modules 99 | # *.iml 100 | # *.ipr 101 | 102 | # CMake 103 | cmake-build-*/ 104 | 105 | # Mongo Explorer plugin 106 | .idea/**/mongoSettings.xml 107 | 108 | # File-based project format 109 | *.iws 110 | 111 | # IntelliJ 112 | out/ 113 | 114 | # mpeltonen/sbt-idea plugin 115 | .idea_modules/ 116 | 117 | # JIRA plugin 118 | atlassian-ide-plugin.xml 119 | 120 | # Cursive Clojure plugin 121 | .idea/replstate.xml 122 | 123 | # SonarLint plugin 124 | .idea/sonarlint/ 125 | 126 | # Crashlytics plugin (for Android Studio and IntelliJ) 127 | com_crashlytics_export_strings.xml 128 | crashlytics.properties 129 | crashlytics-build.properties 130 | fabric.properties 131 | 132 | # Editor-based Rest Client 133 | .idea/httpRequests 134 | 135 | # Android studio 3.1+ serialized cache file 136 | .idea/caches/build_file_checksums.ser 137 | 138 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 134 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/editor.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 580 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/pmlxzj_unlocker.iml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.22.1) 2 | project(pmlxzj_unlocker VERSION 0.1.8) 3 | cmake_policy(SET CMP0135 NEW) 4 | 5 | option(STATIC_ZLIB "static link zlib" OFF) 6 | 7 | set(CMAKE_C_STANDARD 17) 8 | 9 | include(FetchContent) 10 | FetchContent_Declare(zlib 11 | URL https://github.com/madler/zlib/releases/download/v1.3.1/zlib-1.3.1.tar.gz 12 | ) 13 | FetchContent_MakeAvailable(zlib) 14 | 15 | string(CONCAT pmlxzj_unlocker_ver 16 | ${pmlxzj_unlocker_VERSION_MAJOR} "." 17 | ${pmlxzj_unlocker_VERSION_MINOR} "." 18 | ${pmlxzj_unlocker_VERSION_PATCH}) 19 | 20 | add_executable(pmlxzj_unlocker 21 | main.c 22 | pmlxzj.c 23 | pmlxzj_audio.c 24 | pmlxzj_frame.c 25 | cmd_disable_audio.c 26 | cmd_extract_audio.c 27 | cmd_info.c 28 | cmd_unlock_exe.c 29 | cmd_remove_watermark.c 30 | ) 31 | target_compile_definitions(pmlxzj_unlocker PRIVATE PROJECT_VERSION="${pmlxzj_unlocker_ver}") 32 | 33 | if (STATIC_ZLIB) 34 | add_library(ZLIB::ZLIB ALIAS zlibstatic) 35 | message("using static zlib") 36 | else () 37 | add_library(ZLIB::ZLIB ALIAS zlib) 38 | message("using dynamic zlib") 39 | 40 | add_custom_command(TARGET pmlxzj_unlocker POST_BUILD 41 | COMMAND ${CMAKE_COMMAND} -E copy $ 42 | $) 43 | endif () 44 | 45 | target_link_libraries(pmlxzj_unlocker ZLIB::ZLIB) 46 | 47 | if (MSVC) 48 | target_compile_options(pmlxzj_unlocker PRIVATE /W4 /WX) 49 | else () 50 | target_compile_options(pmlxzj_unlocker PRIVATE -Wall -Wextra -Wpedantic -Werror) 51 | if (CMAKE_BUILD_TYPE STREQUAL "Release") 52 | target_link_options(pmlxzj_unlocker PRIVATE -static-libgcc -Wl,--gc-sections) 53 | endif () 54 | endif () 55 | 56 | if (WIN32) 57 | target_link_libraries(pmlxzj_unlocker shell32.lib) 58 | endif () 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 爱飞的猫 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # 解除《屏幕录像专家》编辑锁定 2 | 3 | [![GitHub License](https://img.shields.io/github/license/FlyingRainyCats/pmlxzj_unlocker)](LICENSE) 4 | [![GitHub Release](https://img.shields.io/github/v/release/FlyingRainyCats/pmlxzj_unlocker)](https://github.com/FlyingRainyCats/pmlxzj_unlocker/releases/latest) 5 | [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/FlyingRainyCats/pmlxzj_unlocker/build-msys2.yml)](https://github.com/FlyingRainyCats/pmlxzj_unlocker/actions/workflows/build-msys2.yml) 6 | 7 | 用于解除使用屏幕录像专家对录像进行 “已进行编辑加密” 文件的解密。 8 | 9 | | 解锁前 | 解锁后 | 10 | |:---------------------------:|:------------------------------:| 11 | | ![锁定截图](assets/locked.webp) | ![解锁后截图](assets/unlocked.webp) | 12 | 13 | by 爱飞的猫@52pojie.cn - 仅供学习交流用途 14 | 15 | ## 特性说明 16 | 17 | 支持: 18 | 19 | - 音频提取 (支持使用 WAV、MP3、AAC 音频格式的 EXE 播放器) 20 | - 禁用音频[^disable_audio] 21 | - 解除“编辑加密”锁定 22 | - 解除“播放加密”锁定 (需要提供正确密码 [^password_note] [^non_ascii_password_note]) 23 | 24 | [^password_note]: 密码的第一位字符不参与解密。 25 | [^non_ascii_password_note]: 包含非 ASCII 字符的密码需要储存到文件,并透过 `-P` 指定。 26 | [^disable_audio]: 禁用音频后,《屏幕录像专家》可以将视频转码到无音频的 MP4 文件。 27 | 28 | 测试于下述《屏幕录像专家》版本生成的 EXE 播放器文件: 29 | 30 | - V7.5 Build 200701224 31 | - V2014 Build 0318 32 | - V2023 Build 0828 33 | 34 | 用到了 `zlib` 进行数据解压缩操作。 35 | 36 | ## 两个版本 37 | 38 | 默认分发的二进制文件(通过 GitHub Actions 构建)有两个版本: 39 | 40 | - `pmlxzj_unlocker-i686.exe` 41 | - msys2-mingw32 构建,支持 32 位系统。 42 | - 文件名包含非 ASCII 字符时可能有兼容性问题。 43 | - `pmlxzj_unlocker.exe` 44 | - msys2-ucrt64 构建,对新系统支援较好,不支持 32 位系统。 45 | - 支持包含非 ASCII 字符的文件名。 46 | 47 | ## 编译 48 | 49 | 使用 cmake + mingw 工具链编译。VS 或许可以,未测试。 50 | 51 | ```sh 52 | cmake -B build -DCMAKE_BUILD_TYPE=Release . 53 | cmake --build build 54 | ``` 55 | 56 | ## 使用方法 57 | 58 | ### 解锁视频 59 | 60 | 使用 `unlock` 指令解锁: 61 | 62 | ```shell 63 | ./pmlxzj_unlocker.exe unlock "录像1.exe" "录像1_解锁.exe" 64 | ./pmlxzj_unlocker.exe unlock -p password "录像1.exe" "录像1_解锁.exe" 65 | ./pmlxzj_unlocker.exe unlock -P /path/to/password.txt "录像1.exe" "录像1_解锁.exe" 66 | ``` 67 | 68 | ※ 若密码包含中文或其它非 ASCII 字符,先存储至文件 (GBK 编码),然后透过 `-P` 参数指定。 69 | 70 | 此外可以指定 `-r` 在密码错误时继续、`-v` 输出更多信息。 71 | 72 | ### 提取音频 73 | 74 | 目前只支持下述“声音压缩”选项的 EXE 播放器: 75 | 76 | - 不压缩 (WAV) 77 | - 无损压缩 (WAV + zlib 压缩) 78 | - 有损压缩 (MP3 格式) 79 | - 有损压缩 (AAC 格式) 80 | 81 | 尚未支援: 82 | 83 | - 有损压缩 (TrueSpeech) 84 | 85 | ```shell 86 | ./pmlxzj_unlocker.exe audio-dump "录像1.exe" "录像1.wav" 87 | ``` 88 | 89 | ### 禁用音频 90 | 91 | 如果 EXE 播放器启用了声音压缩,那么《屏幕录像专家》将拒绝转换视频到 MP4 等格式。 92 | 93 | 为了绕过这一限制,可以使用工具将 EXE 播放器中的音频禁用,然后进行转换。 94 | 95 | ```shell 96 | ./pmlxzj_unlocker.exe audio-disable "录像1.exe" "录像1_无音频.exe" 97 | ``` 98 | 99 | ※ 如果处理《屏幕录像专家》7.0 或之前的版本生成的 EXE 播放器,需要使用《屏幕录像专家》升级播放器然后再处理。 100 | 101 | ### 查看信息 102 | 103 | 查看部分读取的信息。 104 | 105 | ```shell 106 | ./pmlxzj_unlocker.exe info "录像1.exe" 107 | ./pmlxzj_unlocker.exe info -v "录像1.exe" 108 | ``` 109 | 110 | 此外可以指定 `-v` 输出更多信息。 111 | 112 | ### 意义不明 113 | 114 | 有很多地方还没搞懂,也有很多参数能控制读取文件的偏移。 115 | 116 | 目前只是对着部分已知的文件进行测试。 117 | 118 | ## 碎碎念 119 | 120 | OBS 永远的神,屏幕录像专家可以说是时代的眼泪了… 121 | 122 | XP 时代应该是它的巅峰(默认 5fps,录教程够了,低资源设备友好)。 123 | 不过感觉从 Win7 开始就有点力不从心 + 摆烂了。 124 | 125 | 本质上只是为了翻录到 MP4 格式方便传输。利用 OBS + 自动翻录脚本也能做到类似的效果。 126 | 127 | 一开始也想过所谓的“高度无损压缩”会是什么黑科技,结果却发现就是简单的 gzip 压缩有点失望。 128 | 129 | 此外逆向过程中发现了其内嵌了两个 txt 文件(换行分割的数组),怀疑是用来重放鼠标点击事件的信息。 130 | 在代码中的 `idx1` 和 `idx2` 为解析后的对应数据。 131 | 132 | ## 致谢 133 | 134 | - Hmily 老师帮忙定位到了关键解密代码和大致解密流程。 135 | -------------------------------------------------------------------------------- /assets/locked.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlyingRainyCats/pmlxzj_unlocker/11930717266edcdc1b488a4845163113fd9ae18b/assets/locked.webp -------------------------------------------------------------------------------- /assets/unlocked.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlyingRainyCats/pmlxzj_unlocker/11930717266edcdc1b488a4845163113fd9ae18b/assets/unlocked.webp -------------------------------------------------------------------------------- /cmake/mingw-w64-x86_64.cmake: -------------------------------------------------------------------------------- 1 | # Sample toolchain file for building for Windows from an Ubuntu Linux system. 2 | # 3 | # Typical usage: 4 | # *) install cross compiler: `sudo apt-get install mingw-w64` 5 | # *) cd build 6 | # *) cmake -DCMAKE_TOOLCHAIN_FILE=~/mingw-w64-x86_64.cmake .. 7 | # This is free and unencumbered software released into the public domain. 8 | 9 | set(CMAKE_SYSTEM_NAME Windows) 10 | set(TOOLCHAIN_PREFIX x86_64-w64-mingw32) 11 | 12 | # cross compilers to use for C, C++ and Fortran 13 | set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-gcc) 14 | set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++) 15 | set(CMAKE_Fortran_COMPILER ${TOOLCHAIN_PREFIX}-gfortran) 16 | set(CMAKE_RC_COMPILER ${TOOLCHAIN_PREFIX}-windres) 17 | 18 | # target environment on the build host system 19 | set(CMAKE_FIND_ROOT_PATH /usr/${TOOLCHAIN_PREFIX}) 20 | set(CMAKE_SYSTEM_PROCESSOR x86_64) 21 | 22 | # modify default behavior of FIND_XXX() commands 23 | set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) 24 | set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) 25 | set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) 26 | -------------------------------------------------------------------------------- /cmd_disable_audio.c: -------------------------------------------------------------------------------- 1 | #include "pmlxzj.h" 2 | #include "pmlxzj_commands.h" 3 | #include "pmlxzj_utils.h" 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | int pmlxzj_cmd_disable_audio(int argc, char** argv) { 10 | if (argc <= 2) { 11 | pmlxzj_usage(argv[0]); 12 | return 1; 13 | } 14 | 15 | const char* exe_input_path = argv[1]; 16 | const char* exe_output_path = argv[2]; 17 | if (strcmp(exe_input_path, exe_output_path) == 0) { 18 | printf("ERROR: input and output file cannot be the same.\n"); 19 | return 1; 20 | } 21 | 22 | FILE* f_src = fopen(exe_input_path, "rb"); 23 | if (f_src == NULL) { 24 | perror("ERROR: open source input"); 25 | return 1; 26 | } 27 | 28 | pmlxzj_state_t app = {0}; 29 | pmlxzj_user_params_t params = {0}; 30 | params.input_file = f_src; 31 | pmlxzj_state_e status = pmlxzj_init(&app, ¶ms); 32 | if (status != PMLXZJ_OK) { 33 | printf("ERROR: Init pmlxzj exe failed: %d\n", status); 34 | fclose(f_src); 35 | return 1; 36 | } 37 | 38 | FILE* f_dst = fopen(exe_output_path, "wb"); 39 | if (f_dst == NULL) { 40 | perror("ERROR: open output"); 41 | fclose(f_src); 42 | return 1; 43 | } 44 | 45 | // Get the size of the input file 46 | fseek(f_src, 0, SEEK_END); 47 | size_t src_file_size = (size_t)ftell(f_src); 48 | fseek(f_src, 0, SEEK_SET); 49 | 50 | // Copy until the start of the data section 51 | pmlxzj_util_copy(f_dst, f_src, app.footer.offset_data_start); 52 | 53 | uint32_t zero = {0}; 54 | fwrite(&zero, sizeof(zero), 1, f_dst); 55 | 56 | // Copy metadata 57 | if (app.audio_metadata_version == PMLXZJ_AUDIO_VERSION_LEGACY) { 58 | // Legacy: audio data followed by metadata and frame data. 59 | fseek(f_src, app.frame_metadata_offset, SEEK_SET); 60 | int64_t metadata_and_frame_size = 61 | (signed)src_file_size - app.frame_metadata_offset - (signed)sizeof(pmlxzj_footer_t); 62 | pmlxzj_util_copy(f_dst, f_src, metadata_and_frame_size); 63 | } else { 64 | // Current: metadata + frame data, followed by audio data, timecodes, and then header. 65 | // no easy way to strip audio data, let's just ignore them for now. 66 | fseek(f_src, sizeof(uint32_t), SEEK_CUR); 67 | int64_t data_size = 68 | (signed)src_file_size - app.footer.offset_data_start - (signed)sizeof(pmlxzj_footer_t) - 4; 69 | pmlxzj_util_copy(f_dst, f_src, data_size); 70 | fseek(f_dst, app.file_size - (long)(sizeof(pmlxzj_footer_t)), SEEK_SET); 71 | } 72 | 73 | pmlxzj_footer_t footer = {0}; 74 | memcpy(&footer, &app.footer, sizeof(footer)); 75 | footer.config.audio_codec = PMLXZJ_AUDIO_TYPE_WAVE_COMPRESSED; 76 | fwrite(&footer, sizeof(footer), 1, f_dst); 77 | 78 | fclose(f_dst); 79 | fclose(f_src); 80 | 81 | return 0; 82 | } 83 | -------------------------------------------------------------------------------- /cmd_extract_audio.c: -------------------------------------------------------------------------------- 1 | #include "pmlxzj.h" 2 | #include "pmlxzj_commands.h" 3 | #include "pmlxzj_enum_names.h" 4 | 5 | #include 6 | #include 7 | 8 | int pmlxzj_cmd_extract_audio(int argc, char** argv) { 9 | if (argc <= 2) { 10 | pmlxzj_usage(argv[0]); 11 | return 1; 12 | } 13 | 14 | const char* exe_input_path = argv[1]; 15 | const char* audio_output_path = argv[2]; 16 | if (strcmp(exe_input_path, audio_output_path) == 0) { 17 | printf("ERROR: input and output file cannot be the same.\n"); 18 | return 1; 19 | } 20 | 21 | FILE* f_src = fopen(exe_input_path, "rb"); 22 | if (f_src == NULL) { 23 | perror("ERROR: open source input"); 24 | return 1; 25 | } 26 | 27 | pmlxzj_state_t app = {0}; 28 | pmlxzj_user_params_t params = {0}; 29 | params.input_file = f_src; 30 | pmlxzj_state_e status = pmlxzj_init(&app, ¶ms); 31 | if (status != PMLXZJ_OK) { 32 | printf("ERROR: Init pmlxzj exe failed: %d (%s)\n", status, pmlxzj_get_state_name(status)); 33 | fclose(f_src); 34 | return 1; 35 | } 36 | status = pmlxzj_init_audio(&app); 37 | if (status != PMLXZJ_OK) { 38 | printf("ERROR: Init pmlxzj audio failed: %d (%s)\n", status, pmlxzj_get_state_name(status)); 39 | fclose(f_src); 40 | return 1; 41 | } 42 | 43 | FILE* f_audio = fopen(audio_output_path, "wb"); 44 | if (f_audio == NULL) { 45 | perror("ERROR: open audio out"); 46 | fclose(f_src); 47 | return 1; 48 | } 49 | 50 | status = pmlxzj_audio_dump_to_file(&app, f_audio); 51 | if (status == PMLXZJ_OK) { 52 | fseek(f_audio, 0, SEEK_END); 53 | long audio_len = ftello(f_audio); 54 | printf("audio dump ok, len = %ld\n", audio_len); 55 | } else { 56 | printf("ERROR: failed to dump: %d (%s)\n", status, pmlxzj_get_state_name(status)); 57 | } 58 | fclose(f_audio); 59 | fclose(f_src); 60 | 61 | return 0; 62 | } 63 | -------------------------------------------------------------------------------- /cmd_info.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "pmlxzj.h" 4 | #include "pmlxzj_audio_aac.h" 5 | #include "pmlxzj_commands.h" 6 | #include "pmlxzj_enum_names.h" 7 | 8 | typedef struct { 9 | bool verbose; 10 | bool print_frames; 11 | bool print_indexes; 12 | } pmlxzj_cmd_print_info_param_t; 13 | 14 | pmlxzj_enumerate_state_e enum_frame_print_info(pmlxzj_state_t* app, 15 | pmlxzj_frame_info_t* frame, 16 | void* extra_callback_data) { 17 | pmlxzj_cmd_print_info_param_t* param = extra_callback_data; 18 | 19 | long frame_start_offset = ftell(app->file); 20 | if (frame->compressed_size > 10240) { 21 | long patch_start_offset = frame_start_offset + (long)frame->compressed_size / 2; 22 | printf("frame #%d, image #%d (len=(0x%x, 0x%x), offset=0x%x, patch=0x%x)\n", frame->frame_id, frame->image_id, 23 | frame->compressed_size, frame->decompressed_size, (int)frame_start_offset, (int)patch_start_offset); 24 | } else if (param->verbose) { 25 | printf("frame #%d, image #%d (len=(0x%x, 0x%x), offset=0x%x)\n", frame->frame_id, frame->image_id, 26 | frame->compressed_size, frame->decompressed_size, (int)frame_start_offset); 27 | } 28 | 29 | return PMLXZJ_ENUM_CONTINUE; 30 | } 31 | 32 | int pmlxzj_cmd_print_info(int argc, char** argv) { 33 | pmlxzj_cmd_print_info_param_t param = {0}; 34 | int option = -1; 35 | while ((option = getopt(argc, argv, "vfi")) != -1) { 36 | switch (option) { 37 | case 'v': 38 | param.verbose = true; 39 | break; 40 | case 'f': 41 | param.print_frames = true; 42 | break; 43 | case 'i': 44 | param.print_indexes = true; 45 | break; 46 | default: 47 | fprintf(stderr, "ERROR: unknown option '-%c'\n", optopt); 48 | pmlxzj_usage(argv[0]); 49 | return 1; 50 | } 51 | } 52 | 53 | if (argc < optind + 1) { 54 | fprintf(stderr, "ERROR: 'input' required.\n"); 55 | pmlxzj_usage(argv[0]); 56 | return 1; 57 | } 58 | 59 | FILE* f_src = fopen(argv[optind], "rb"); 60 | if (f_src == NULL) { 61 | perror("ERROR: open source input"); 62 | return 1; 63 | } 64 | 65 | pmlxzj_state_t app = {0}; 66 | pmlxzj_user_params_t params = {0}; 67 | params.input_file = f_src; 68 | pmlxzj_state_e status = pmlxzj_init_all(&app, ¶ms); 69 | if (status != PMLXZJ_OK) { 70 | printf("ERROR: Init pmlxzj failed: %d (%s)\n", status, pmlxzj_get_state_name(status)); 71 | return 1; 72 | } 73 | printf("\n"); 74 | 75 | if (param.print_indexes) { 76 | printf("idx1(len=%d):\n", (int)app.idx1_count); 77 | for (size_t i = 0; i < app.idx1_count; i++) { 78 | printf(" idx1[%3d]: %11d / 0x%08x\n", (int)i, app.idx1[i], app.idx1[i]); 79 | } 80 | printf("idx2(len=%d):\n", (int)app.idx2_count); 81 | for (size_t i = 0; i < 20; i++) { 82 | printf(" idx2[%3d]: %11d / 0x%08x\n", (int)i, app.idx2[i], app.idx2[i]); 83 | } 84 | } 85 | 86 | printf("offset: 0x%x\n", app.footer.offset_data_start); 87 | printf("frame:\n"); 88 | printf(" offset: 0x%lx\n", app.first_frame_offset); 89 | printf(" type: %d\n", app.footer.config.image_compress_type); 90 | 91 | printf("audio:\n"); 92 | printf(" offset: "); 93 | if (app.audio_metadata_offset) { 94 | printf("0x%08lx\n", app.audio_metadata_offset); 95 | } else { 96 | printf("(none)\n"); 97 | } 98 | printf(" codec: %u # %s\n", app.footer.config.audio_codec, 99 | pmlxzj_get_audio_codec_name(app.footer.config.audio_codec)); 100 | 101 | switch (app.footer.config.audio_codec) { 102 | case PMLXZJ_AUDIO_TYPE_WAVE_RAW: { 103 | pmlxzj_audio_wav_t* audio = &app.audio.wav; 104 | printf(" offset: 0x%08lx\n", audio->offset); 105 | printf(" size: 0x%08x\n", audio->size); 106 | break; 107 | } 108 | 109 | case PMLXZJ_AUDIO_TYPE_WAVE_COMPRESSED: { 110 | pmlxzj_audio_wav_zlib_t* audio = &app.audio.wav_zlib; 111 | printf(" offset: 0x%08lx\n", audio->offset); 112 | printf(" segments: 0x%08x\n", audio->segment_count); 113 | break; 114 | } 115 | 116 | case PMLXZJ_AUDIO_TYPE_LOSSY_MP3: { 117 | pmlxzj_audio_mp3_t* audio = &app.audio.mp3; 118 | printf(" offset: 0x%08lx\n", audio->offset); 119 | printf(" size: 0x%08x\n", audio->size); 120 | if (param.verbose) { 121 | printf(" chunks:\n"); 122 | for (uint32_t i = 0; i < audio->segment_count; i++) { 123 | printf(" mp3_chunk[%04u].offset: 0x%08x\n", i, audio->offsets[i]); 124 | } 125 | } 126 | 127 | break; 128 | } 129 | 130 | case PMLXZJ_AUDIO_TYPE_LOSSY_AAC: { 131 | pmlxzj_audio_aac_t* audio = &app.audio.aac; 132 | 133 | printf(" offset: 0x%08lx\n", audio->offset); 134 | printf(" size: 0x%08x\n", audio->size); 135 | printf(" segments: 0x%08x\n", audio->segment_count); 136 | printf(" format: "); 137 | for (size_t i = 0; i < sizeof(audio->format); i++) { 138 | printf("%02x ", audio->format[i]); 139 | } 140 | printf("\n"); 141 | printf(" profile: %2d (%s)\n", audio->config.profile_id, 142 | pmlxzj_adts_get_profile_name(audio->config.profile_id)); 143 | printf(" sample rate: %2d (%d Hz)\n", audio->config.sample_rate_id, 144 | pmlxzj_adts_get_sample_rate(audio->config.sample_rate_id)); 145 | printf(" channel: %2d\n", audio->config.channels_id); 146 | 147 | break; 148 | } 149 | } 150 | 151 | printf("encrypt edit_lock nonce: "); 152 | if (app.footer.edit_lock_nonce) { 153 | printf("0x%08x\n", app.footer.edit_lock_nonce); 154 | } else { 155 | printf("(unset)\n"); 156 | } 157 | printf("encrypt play_lock password checksum: "); 158 | if (app.footer.play_lock_password_checksum) { 159 | printf("0x%08x\n", app.footer.play_lock_password_checksum); 160 | } else { 161 | printf("(unset)\n"); 162 | } 163 | 164 | if (param.print_frames) { 165 | pmlxzj_enumerate_images(&app, enum_frame_print_info, ¶m); 166 | } 167 | fclose(f_src); 168 | 169 | return 0; 170 | } 171 | -------------------------------------------------------------------------------- /cmd_remove_watermark.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "pmlxzj.h" 4 | #include "pmlxzj_commands.h" 5 | #include "pmlxzj_utils.h" 6 | 7 | typedef struct { 8 | bool verbose; 9 | bool remove_unregistered_watermark; 10 | bool remove_playback_text_watermark; 11 | } pmlxzj_cmd_remove_watermark_param_t; 12 | 13 | int pmlxzj_cmd_remove_watermark(int argc, char** argv) { 14 | pmlxzj_cmd_remove_watermark_param_t param = {0}; 15 | int option = -1; 16 | while ((option = getopt(argc, argv, "vrt")) != -1) { 17 | switch (option) { 18 | case 'v': 19 | param.verbose = true; 20 | break; 21 | case 'r': 22 | param.remove_unregistered_watermark = true; 23 | break; 24 | case 't': 25 | param.remove_playback_text_watermark = true; 26 | break; 27 | default: 28 | fprintf(stderr, "ERROR: unknown option '-%c'\n", optopt); 29 | pmlxzj_usage(argv[0]); 30 | return 1; 31 | } 32 | } 33 | 34 | // Default to remove both watermarks, if none specified 35 | if (!param.remove_unregistered_watermark && !param.remove_playback_text_watermark) { 36 | param.remove_unregistered_watermark = true; 37 | param.remove_playback_text_watermark = true; 38 | } 39 | 40 | if (argc < optind + 2) { 41 | fprintf(stderr, "ERROR: missing parameters for input and output"); 42 | pmlxzj_usage(argv[0]); 43 | return 1; 44 | } 45 | 46 | const char* exe_input_path = argv[optind]; 47 | const char* exe_output_path = argv[optind + 1]; 48 | if (strcmp(exe_input_path, exe_output_path) == 0) { 49 | printf("ERROR: input and output file cannot be the same.\n"); 50 | return 1; 51 | } 52 | 53 | FILE* f_src = fopen(exe_input_path, "rb"); 54 | if (f_src == NULL) { 55 | perror("ERROR: open source input"); 56 | return 1; 57 | } 58 | 59 | FILE* f_dst = fopen(exe_output_path, "wb"); 60 | if (f_dst == NULL) { 61 | perror("ERROR: open dest input"); 62 | fclose(f_src); 63 | return 1; 64 | } 65 | 66 | pmlxzj_state_t app = {0}; 67 | pmlxzj_user_params_t params = {0}; 68 | params.input_file = f_src; 69 | pmlxzj_state_e status = pmlxzj_init_all(&app, ¶ms); 70 | if (status != PMLXZJ_OK) { 71 | printf("ERROR: Init pmlxzj exe failed: %d\n", status); 72 | fclose(f_src); 73 | return 1; 74 | } 75 | 76 | pmlxzj_watermark_t watermark = app.watermark; 77 | if (param.remove_unregistered_watermark) { 78 | static uint8_t lic_data_1[20] = {0x41, 0x69, 0x46, 0x65, 0x69, 0x44, 0x65, 0x4D, 0x61, 0x6F, 79 | 0x40, 0x35, 0x32, 0x70, 0x6F, 0x6A, 0x69, 0x65, 0x00, 0x00}; 80 | static uint8_t lic_data_2[20] = {0x4C, 0x53, 0x48, 0x4E, 0x48, 0x53, 0x47, 0x4D, 0x48, 0x00, 81 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; 82 | memcpy(watermark.lic_data_1, lic_data_1, sizeof(lic_data_1)); 83 | memcpy(watermark.lic_data_2, lic_data_2, sizeof(lic_data_2)); 84 | } 85 | 86 | if (param.remove_playback_text_watermark) { 87 | static uint8_t blank_playback_watermark[20] = {0x64, 0x21, 0x41, 0x69, 0x46, 0x65, 0x69, 0x44, 0x65, 0x4D, 88 | 0x61, 0x6F, 0x40, 0x35, 0x32, 0x70, 0x6F, 0x6A, 0x69, 0x65}; 89 | memcpy(watermark.user_watermark, blank_playback_watermark, sizeof(blank_playback_watermark)); 90 | } 91 | 92 | pmlxzj_util_copy_file(f_dst, f_src); 93 | fseek(f_dst, app.watermark_offset, SEEK_SET); 94 | fwrite(&watermark, sizeof(watermark), 1, f_dst); 95 | 96 | return 0; 97 | } 98 | -------------------------------------------------------------------------------- /cmd_unlock_exe.c: -------------------------------------------------------------------------------- 1 | #include "pmlxzj.h" 2 | #include "pmlxzj_commands.h" 3 | #include "pmlxzj_enum_names.h" 4 | #include "pmlxzj_utils.h" 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | typedef struct { 11 | bool verbose; 12 | bool resume_on_bad_password; 13 | char password[21]; 14 | FILE* file_destination; 15 | } pmlxzj_cmd_unlock_exe_param_t; 16 | 17 | pmlxzj_enumerate_state_e enum_frame_patch(pmlxzj_state_t* app, pmlxzj_frame_info_t* frame, void* extra) { 18 | pmlxzj_cmd_unlock_exe_param_t* cli_params = extra; 19 | long frame_start_offset = ftell(app->file); 20 | 21 | if (frame->compressed_size > 10240) { 22 | long key_pos = frame_start_offset + 4; 23 | long cipher_pos = frame_start_offset + (long)frame->compressed_size / 2; 24 | 25 | char frame_key[20]; 26 | char frame_ciphered[20]; 27 | fseek(app->file, key_pos, SEEK_SET); 28 | fread(frame_key, sizeof(frame_key), 1, app->file); 29 | fseek(app->file, cipher_pos, SEEK_SET); 30 | fread(frame_ciphered, sizeof(frame_ciphered), 1, app->file); 31 | 32 | for (int i = 0; i < 20; i++) { 33 | frame_ciphered[i] = (char)(frame_ciphered[i] ^ frame_key[i] ^ app->nonce_buffer[i]); 34 | } 35 | fseek(cli_params->file_destination, cipher_pos, SEEK_SET); 36 | fwrite(frame_ciphered, sizeof(frame_ciphered), 1, cli_params->file_destination); 37 | 38 | printf("frame #%d, image #%d (len=(0x%x, 0x%x), offset=0x%x, patch=0x%x)\n", frame->frame_id, frame->image_id, 39 | frame->compressed_size, frame->decompressed_size, (int)frame_start_offset, (int)cipher_pos); 40 | } else if (cli_params->verbose) { 41 | printf("frame #%d, image #%d (len=(0x%x, 0x%x), offset=0x%x)\n", frame->frame_id, frame->image_id, 42 | frame->compressed_size, frame->decompressed_size, (int)frame_start_offset); 43 | } 44 | 45 | return PMLXZJ_ENUM_CONTINUE; 46 | } 47 | 48 | int pmlxzj_cmd_unlock_exe(int argc, char** argv) { 49 | pmlxzj_cmd_unlock_exe_param_t cli_params = {0}; 50 | 51 | int opt; 52 | while ((opt = getopt(argc, argv, "vrp:P:")) != -1) { 53 | switch (opt) { 54 | case 'v': 55 | cli_params.verbose = true; 56 | break; 57 | 58 | case 'r': 59 | cli_params.resume_on_bad_password = true; 60 | break; 61 | 62 | case 'p': 63 | strncpy(cli_params.password, optarg, sizeof(cli_params.password) - 1); 64 | break; 65 | 66 | case 'P': { 67 | FILE* f_password = fopen(optarg, "rb"); 68 | if (f_password == NULL) { 69 | perror("read password file"); 70 | return 1; 71 | } 72 | fread(cli_params.password, sizeof(cli_params.password) - 1, 1, f_password); 73 | fclose(f_password); 74 | break; 75 | } 76 | 77 | default: 78 | fprintf(stderr, "ERROR: unknown option '-%c'\n", optopt); 79 | pmlxzj_usage(argv[0]); 80 | break; 81 | } 82 | } 83 | 84 | if (argc < optind + 2) { 85 | fprintf(stderr, "ERROR: missing parameters for input and output"); 86 | pmlxzj_usage(argv[0]); 87 | return 1; 88 | } 89 | 90 | const char* exe_input_path = argv[optind]; 91 | const char* exe_output_path = argv[optind + 1]; 92 | if (strcmp(exe_input_path, exe_output_path) == 0) { 93 | printf("ERROR: input and output file cannot be the same.\n"); 94 | return 1; 95 | } 96 | 97 | FILE* f_src = fopen(exe_input_path, "rb"); 98 | if (f_src == NULL) { 99 | perror("ERROR: open source input"); 100 | return 1; 101 | } 102 | 103 | FILE* f_dst = fopen(exe_output_path, "wb"); 104 | if (f_dst == NULL) { 105 | perror("ERROR: open dest input"); 106 | fclose(f_src); 107 | return 1; 108 | } 109 | 110 | cli_params.file_destination = f_dst; 111 | 112 | pmlxzj_state_t app = {0}; 113 | pmlxzj_user_params_t pmlxzj_param = {0}; 114 | pmlxzj_param.input_file = f_src; 115 | pmlxzj_param.resume_on_bad_password = cli_params.resume_on_bad_password; 116 | memcpy(pmlxzj_param.password, cli_params.password, sizeof(pmlxzj_param.password)); 117 | pmlxzj_state_e status = pmlxzj_init(&app, &pmlxzj_param); 118 | if (status != PMLXZJ_OK) { 119 | printf("ERROR: Init failed (exe): %d (%s)\n", status, pmlxzj_get_state_name(status)); 120 | fclose(f_dst); 121 | fclose(f_src); 122 | return 1; 123 | } 124 | status = pmlxzj_init_frame(&app); 125 | if (status != PMLXZJ_OK) { 126 | printf("ERROR: Init failed (frame): %d (%s)\n", status, pmlxzj_get_state_name(status)); 127 | fclose(f_dst); 128 | fclose(f_src); 129 | return 1; 130 | } 131 | 132 | pmlxzj_util_copy_file(f_dst, f_src); 133 | 134 | if (app.encrypt_mode == 1 || app.encrypt_mode == 2) { 135 | pmlxzj_enumerate_images(&app, enum_frame_patch, &cli_params); 136 | 137 | pmlxzj_footer_t new_footer = {0}; 138 | memcpy(&new_footer, &app.footer, sizeof(new_footer)); 139 | new_footer.edit_lock_nonce = 0; 140 | new_footer.play_lock_password_checksum = 0; 141 | fseek(f_dst, app.file_size - (long)(sizeof(pmlxzj_footer_t)), SEEK_SET); 142 | fwrite(&new_footer, sizeof(new_footer), 1, f_dst); 143 | } else { 144 | printf("error: edit_lock_nonce is zero. Unsupported cipher or not encrypted.\n"); 145 | } 146 | 147 | fclose(f_dst); 148 | fclose(f_src); 149 | 150 | return 0; 151 | } 152 | -------------------------------------------------------------------------------- /docs/exe_player_spec.adoc: -------------------------------------------------------------------------------- 1 | = 《屏幕录像专家》EXE 播放器文件格式 2 | 3 | 该文档简单介绍了利用“屏幕录像专家”生成的 EXE 播放器文件的部分格式信息。 4 | 5 | == EXE 与 LXE 6 | 7 | EXE 播放器其实就是一个 `LXE` 数据播放器。将 EXE 转换为 LXE 格式只是将 EXE 可执行程序部分进行加密,余下数据部分一致。 8 | 9 | == 数据“尾” 10 | 11 | 录像数据通常在 EXE 的 Overlay 区域。该区域的起始标识符为 `DATASTART\0`: 12 | 13 | [source,text] 14 | ---- 15 | +---------+--------------------------------------------------+------------------+ 16 | | 地址 | 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f | 预览 | 17 | +---------+--------------------------------------------------+------------------+ 18 | |00A:AE00 | 44 41 54 41 53 54 41 52 54 00 BB CD 28 FE 01 00 | DATASTART.»Í(þ.. | 19 | +---------+--------------------------------------------------+------------------+ 20 | ---- 21 | 22 | 不过 EXE 并不是从该数据头开始读,而是从文件结尾的 `0x2C` 字节开始: 23 | 24 | [source,text] 25 | ---- 26 | +------+--------------------------------------------------+------------------+ 27 | | 地址 | 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f | 预览 | 28 | +------+--------------------------------------------------+------------------+ 29 | | 0000 | 25 3B 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | %;.............. | 30 | | 0010 | 00 00 00 00 08 00 00 00 08 00 00 00 0A AE 0A 00 | ................ | 31 | | 0020 | 70 6D 6C 78 7A 6A 74 6C 78 00 00 00 | pmlxzjtlx... | 32 | -------+--------------------------------------------------+------------------- 33 | ---- 34 | 35 | - 偏移 `0x00` (`uint32_t`) 是 “播放加密” 锁定时的随机值,记作 `nonce`。 36 | - 偏移 `0x1C` (`uint32_t`) 是数据起始位置偏移。此处为 `0x0aae0a`。 37 | - 偏移 `0x20` (`char[0x0c]`) 是播放器特征码。 38 | 39 | == 数据“头” 40 | 41 | 回到数据头指向的 `0x0aae0a`,此处的数据部分没有深入研究,只给出研究过的那些: 42 | 43 | [source,c] 44 | ----- 45 | struct header { 46 | uint32_t audio_offset; 47 | struct { 48 | uint32_t field_0; 49 | uint32_t field_4; 50 | uint32_t field_8; 51 | int32_t total_frame_count; 52 | uint32_t field_10; 53 | uint32_t field_14; 54 | uint32_t field_18; 55 | uint32_t field_1C; 56 | uint32_t field_20; 57 | uint32_t field_24; 58 | } field_14D8; 59 | char[20] field_2C; 60 | char[20] field_40; 61 | char[40] field_54; 62 | uint32_t field_7C; 63 | uint32_t field_80; 64 | uint32_t field_84; 65 | char[20] field_88; 66 | uint32_t field_9C; 67 | uint8_t field_A0; 68 | uint8_t field_A1; 69 | uint8_t field_A2; 70 | uint8_t field_A3; 71 | 72 | if (field_14D8.field_24) { 73 | float unk1; 74 | float unk2; 75 | uint32_t stream2_len; 76 | uint8_t stream2[stream2_len]; 77 | } 78 | }; 79 | ----- 80 | 81 | 过了 `header` 结构体后,就是视频数据了。 82 | 83 | == 视频数据 84 | 85 | 就是第一帧 `first_frame` 的内容,然后后面就是重复的 `other_frame` 数据: 86 | 87 | [source,c] 88 | ---- 89 | struct frame_data { 90 | uint32_t len; 91 | uint32_t decompressed_len; 92 | uint8_t data[len - 4]; 93 | }; 94 | 95 | // 入口的第一帧 96 | struct first_frame { 97 | frame_data frame; // 帧信息 98 | int32_t frame_id; // 帧序号 99 | 100 | // field_24 == 1 的情况,会有两个额外的 f32 数据。 101 | if (field_24 == 1) { 102 | float unknown_1; 103 | float unknown_2; 104 | } 105 | } 106 | 107 | // 后续则是一直读取 `other_frame` 解构,直到结束 108 | struct other_frame { 109 | // 未知数据流 110 | uint32_t stream2_len; 111 | uint8_t stream2[stream2_len]; 112 | 113 | uint32_t frame_state; // 帧状态 114 | 115 | if (frame_state > 0) { 116 | RECT patch_cord; // 坐标相关 117 | 118 | while (frame_state > 0) { 119 | frame_data frame; // 帧信息 120 | int32_t frame_id; // 帧序号 121 | } 122 | } 123 | 124 | // field_24 == 1 的情况,会有两个额外的 f32 数据。 125 | if (field_24 == 1) { 126 | float unknown_1; 127 | float unknown_2; 128 | } 129 | }; 130 | ---- 131 | 132 | 读取完最后一帧时,`other_frame.frame_id` 等于 `-(field_14d8.total_frame_count - 1)`。 133 | 134 | == 音频数据 135 | 136 | 音频数据偏移存储在初始偏移处(如下方地址 `00AAE0A` 处): 137 | 138 | [source,text] 139 | ---- 140 | +---------+--------------------------------------------------+------------------+ 141 | | 地址 | 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f | 预览 | 142 | +---------+--------------------------------------------------+------------------+ 143 | |00A:AE00 | 44 41 54 41 53 54 41 52 54 00 BB CD 28 FE 01 00 | DATASTART.»Í(þ.. | 144 | +---------+--------------------------------------------------+------------------+ 145 | ---- 146 | 147 | 其中 `BB CD 28 FE` 为 `0xfe28cdbb`,取对应负数得到偏移 `0x1d73245`。 148 | 149 | [source,text] 150 | ---- 151 | +----------+--------------------------------------------------+ 152 | | 地址 | 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f | 153 | +----------+--------------------------------------------------+ 154 | | 1D7:3240 | F8 02 00 00 18 59 00 00 78 DA 85 | 155 | | 1D7:3250 | 7D CB AE 64 4B 73 56 44 AE EA 73 6C 10 60 83 84 | 156 | +----------+--------------------------------------------------+ 157 | ---- 158 | 159 | 第一个值 `F8 02 00 00` 表示音频共有 `0x2f8` 段,随后每一段都是地址前缀编码的数据。 160 | 161 | [source,c] 162 | ----- 163 | struct audio_data { 164 | uint32_t segment_count; 165 | struct audio_segment { 166 | uint32_t len; 167 | uint8_t data[len]; 168 | } segments[segment_count]; 169 | }; 170 | ----- 171 | 172 | 每一段通过 GZip 解压,然后拼接就能得到完整的 `.wav` 格式音频了。 173 | 174 | == “编辑加密” 锁定 175 | 176 | * 每个“大帧”会加密中间的 20 字节 177 | ** “大帧”的判定条件是 `frame.len > 10240`。 178 | ** `&frame.data[frame.len / 2 - 4 .. frame.len / 2 + 16]`。 179 | * 加密算法是 `xor`,密钥为 `(&data[0..20] XOR nonce_key)`。 180 | 181 | `nonce_key` 的获取方式: 182 | 183 | [source,c] 184 | ---- 185 | // 文件结尾 -0x2c 偏移处的值 186 | uint32_t nonce = 0x3b25; 187 | 188 | char nonce_key[20]; 189 | char buffer[21] = { 0 }; 190 | snprintf(buffer, 20, "%d", nonce); 191 | nonce_key = reverse(&buffer[1..21]); 192 | ---- 193 | -------------------------------------------------------------------------------- /main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "pmlxzj_commands.h" 5 | #include "pmlxzj_win32.h" 6 | 7 | #ifndef PROJECT_VERSION 8 | #define PROJECT_VERSION "0.0.0-unknown" 9 | #endif 10 | 11 | #ifdef _MSC_VER 12 | #pragma execution_character_set("utf-8") 13 | #endif 14 | 15 | struct pmlxzj_commands_t { 16 | const char* name; 17 | int (*handler)(int argc, char** argv); 18 | }; 19 | 20 | static struct pmlxzj_commands_t g_pmlxzj_commands[] = { 21 | {.name = "info", .handler = pmlxzj_cmd_print_info}, 22 | {.name = "unlock", .handler = pmlxzj_cmd_unlock_exe}, 23 | {.name = "audio-dump", .handler = pmlxzj_cmd_extract_audio}, 24 | {.name = "audio-disable", .handler = pmlxzj_cmd_disable_audio}, 25 | {.name = "remove-watermark", .handler = pmlxzj_cmd_remove_watermark}, 26 | }; 27 | 28 | int main(int argc, char** argv) { 29 | FixWindowsUnicodeSupport(&argv); 30 | 31 | printf("pmlxzj_unlocker v" PROJECT_VERSION " by 爱飞的猫@52pojie.cn (FlyingRainyCats)\n"); 32 | if (argc <= 2) { 33 | pmlxzj_usage(argv[0]); 34 | return 1; 35 | } 36 | 37 | char* command = argv[1]; 38 | argv[1] = argv[0]; 39 | argv++; 40 | argc--; 41 | 42 | const size_t command_count = sizeof(g_pmlxzj_commands) / sizeof(g_pmlxzj_commands[0]); 43 | for (size_t i = 0; i < command_count; i++) { 44 | struct pmlxzj_commands_t* cmd = &g_pmlxzj_commands[i]; 45 | if (strcmp(command, cmd->name) == 0) { 46 | return cmd->handler(argc, argv); 47 | } 48 | } 49 | 50 | pmlxzj_usage(argv[0]); 51 | return 1; 52 | } 53 | -------------------------------------------------------------------------------- /pmlxzj.c: -------------------------------------------------------------------------------- 1 | #include "pmlxzj.h" 2 | #include "pmlxzj_enum_names.h" 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | int scan_rows(uint32_t* dest_array, const char* str, size_t len) { 9 | const char* end = str + len - 1; 10 | 11 | uint32_t value = 0; 12 | int rows = 0; 13 | const char* p = str; 14 | while (p < end) { 15 | if (p[0] == '\r' && p[1] == '\n') { 16 | if (dest_array) { 17 | *dest_array++ = value; 18 | } 19 | p += 2; 20 | rows++; 21 | value = 0; 22 | continue; 23 | } 24 | value = value * 10 + (*p - '0'); 25 | p++; 26 | } 27 | 28 | return rows; 29 | } 30 | 31 | pmlxzj_state_e scan_rows_to_u32_array(uint32_t** scanned_array, size_t* count, const char* str, size_t len) { 32 | *count = 0; 33 | int c = scan_rows(NULL, str, len); 34 | *scanned_array = calloc(c, sizeof(uint32_t)); 35 | if (*scanned_array == NULL) { 36 | return PMLXZJ_ALLOCATE_INDEX_LIST_ERROR; 37 | } 38 | *count = c; 39 | scan_rows(*scanned_array, str, len); 40 | return PMLXZJ_OK; 41 | } 42 | 43 | pmlxzj_state_e scan_file_u32_array(uint32_t** scanned_array, size_t* count, FILE* f, long* offset) { 44 | long off = *offset - 4; 45 | fseek(f, off, SEEK_SET); 46 | uint32_t text_count = 0; 47 | fread(&text_count, sizeof(text_count), 1, f); 48 | off -= (long)text_count; 49 | char* text = malloc(text_count + 1); 50 | if (text == NULL) { 51 | return PMLXZJ_SCAN_U32_ARRAY_ALLOC_ERROR; 52 | } 53 | text[text_count] = 0; 54 | fseek(f, off, SEEK_SET); 55 | fread(text, 1, text_count, f); 56 | pmlxzj_state_e result = scan_rows_to_u32_array(scanned_array, count, text, text_count); 57 | free(text); 58 | if (result != PMLXZJ_OK) { 59 | *offset = 0; 60 | free(*scanned_array); 61 | *scanned_array = NULL; 62 | } else { 63 | *offset = off; 64 | } 65 | return result; 66 | } 67 | 68 | pmlxzj_state_e pmlxzj_init(pmlxzj_state_t* ctx, pmlxzj_user_params_t* params) { 69 | FILE* f_src = params->input_file; 70 | 71 | memset(ctx, 0, sizeof(*ctx)); 72 | ctx->file = f_src; 73 | 74 | fseek(f_src, 0, SEEK_END); 75 | ctx->file_size = ftell(f_src); 76 | 77 | fseek(f_src, -(int)sizeof(ctx->footer), SEEK_END); 78 | fread(&ctx->footer, sizeof(ctx->footer), 1, f_src); 79 | 80 | if (strcmp(ctx->footer.magic, "pmlxzjtlx") != 0) { 81 | return PMLXZJ_MAGIC_NOT_MATCH; 82 | } 83 | 84 | if (ctx->footer.edit_lock_nonce) { 85 | ctx->encrypt_mode = 1; 86 | char buffer[21] = {0}; 87 | snprintf(buffer, sizeof(buffer) - 1, "%d", ctx->footer.edit_lock_nonce); 88 | for (int i = 1; i < 20; i++) { 89 | ctx->nonce_buffer[i] = buffer[20 - i]; 90 | } 91 | } else if (ctx->footer.play_lock_password_checksum) { 92 | ctx->encrypt_mode = 2; 93 | 94 | uint32_t expected_checksum = ctx->footer.play_lock_password_checksum; 95 | uint32_t actual_checksum = pmlxzj_password_checksum(params->password); 96 | if (actual_checksum != expected_checksum) { 97 | fprintf(stderr, "ERROR: Password checksum mismatch.\n"); 98 | fprintf(stderr, " Expected: 0x%08x, got 0x%08x.\n", expected_checksum, actual_checksum); 99 | if (params->resume_on_bad_password) { 100 | fprintf(stderr, "WARN: Continue without valid password...\n"); 101 | } else { 102 | return PMLXZJ_PASSWORD_ERROR; 103 | } 104 | } 105 | 106 | char buffer[21] = {0}; 107 | strncpy(buffer, params->password, sizeof(buffer)); 108 | for (int i = 1; i < 20; i++) { 109 | ctx->nonce_buffer[i] = buffer[20 - i]; 110 | } 111 | } else { 112 | ctx->encrypt_mode = 0; 113 | } 114 | 115 | fseek(f_src, (long)ctx->footer.offset_data_start, SEEK_SET); 116 | int32_t variant_i32 = 0; 117 | fread(&variant_i32, sizeof(variant_i32), 1, f_src); 118 | if (variant_i32 <= 0) { 119 | // New format 120 | ctx->audio_metadata_version = PMLXZJ_AUDIO_VERSION_CURRENT; 121 | ctx->audio_metadata_offset = (long)-variant_i32; 122 | ctx->frame_metadata_offset = (long)ctx->footer.offset_data_start + (long)sizeof(variant_i32); 123 | } else { 124 | // Legacy format 125 | ctx->audio_metadata_version = PMLXZJ_AUDIO_VERSION_LEGACY; 126 | ctx->frame_metadata_offset = (long)variant_i32; 127 | ctx->audio_metadata_offset = (long)ctx->footer.offset_data_start + (long)sizeof(variant_i32); 128 | } 129 | ctx->watermark_offset = ctx->frame_metadata_offset + (long)sizeof(ctx->field_14d8); 130 | 131 | long offset = (long)(ctx->file_size - sizeof(ctx->footer)); 132 | pmlxzj_state_e status = scan_file_u32_array(&ctx->idx1, &ctx->idx1_count, f_src, &offset); 133 | if (status != PMLXZJ_OK) { 134 | return status; 135 | } 136 | status = scan_file_u32_array(&ctx->idx2, &ctx->idx2_count, f_src, &offset); 137 | if (status != PMLXZJ_OK) { 138 | return status; 139 | } 140 | 141 | return PMLXZJ_OK; 142 | } 143 | 144 | pmlxzj_state_e pmlxzj_init_all(pmlxzj_state_t* ctx, pmlxzj_user_params_t* params) { 145 | pmlxzj_state_e status = pmlxzj_init(ctx, params); 146 | if (status != PMLXZJ_OK) { 147 | return status; 148 | } 149 | status = pmlxzj_init_audio(ctx); 150 | if (status != PMLXZJ_OK && status != PMLXZJ_NO_AUDIO) { 151 | return status; 152 | } 153 | status = pmlxzj_init_frame(ctx); 154 | if (status != PMLXZJ_OK) { 155 | return status; 156 | } 157 | 158 | return PMLXZJ_OK; 159 | } 160 | 161 | uint32_t pmlxzj_password_checksum(const char* password) { 162 | uint32_t checksum = 0x7D5; 163 | for (int i = 0; i < 20 && *password; i++) { 164 | uint8_t chr = *password++; 165 | checksum += chr * (i + i / 5 + 1); 166 | } 167 | return checksum; 168 | } 169 | -------------------------------------------------------------------------------- /pmlxzj.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | typedef enum { 8 | PMLXZJ_AUDIO_TYPE_WAVE_RAW = 1, 9 | PMLXZJ_AUDIO_TYPE_WAVE_COMPRESSED = 2, 10 | PMLXZJ_AUDIO_TYPE_LOSSY_MP3 = 5, 11 | PMLXZJ_AUDIO_TYPE_LOSSY_TRUE_SPEECH = 6, 12 | PMLXZJ_AUDIO_TYPE_LOSSY_AAC = 7, 13 | } pmlxzj_audio_codec_e; 14 | 15 | typedef enum { 16 | PMLXZJ_AUDIO_VERSION_LEGACY = 0, 17 | PMLXZJ_AUDIO_VERSION_CURRENT = 1, 18 | } pmlxzj_audio_version; 19 | 20 | #pragma pack(push, 1) 21 | // size = 0xB4 22 | typedef struct { 23 | uint32_t color; 24 | uint32_t wnd_state; 25 | uint32_t field_8; 26 | uint32_t field_C; 27 | uint32_t field_10; 28 | uint32_t border_style_2; 29 | uint32_t field_18; 30 | uint32_t field_1C; 31 | uint32_t field_20; 32 | uint32_t field_24; 33 | uint32_t border_style_1; 34 | uint32_t field_2C; 35 | uint32_t field_30; 36 | uint32_t field_34; 37 | char wnd_title[24]; 38 | uint32_t image_compress_type; 39 | uint32_t audio_codec; 40 | uint32_t field_58; 41 | uint32_t field_5C; 42 | uint32_t field_60; 43 | uint32_t field_64; 44 | uint32_t field_68; 45 | uint32_t field_6C; 46 | uint32_t field_70; 47 | uint32_t field_74; 48 | uint32_t field_78; 49 | uint32_t field_7C; 50 | uint32_t field_80; 51 | uint32_t field_84; 52 | uint32_t field_88; 53 | uint32_t field_8C; 54 | uint32_t field_90; 55 | uint32_t field_94; 56 | uint32_t field_98; 57 | uint32_t field_9C; 58 | uint32_t field_A0; 59 | uint32_t field_A4; 60 | uint32_t field_A8; 61 | uint32_t field_AC; 62 | uint32_t field_B0; 63 | } pmlxzj_initial_ui_state_t; 64 | 65 | typedef struct { 66 | pmlxzj_initial_ui_state_t config; 67 | uint32_t edit_lock_nonce; 68 | uint32_t play_lock_password_checksum; 69 | uint32_t key_3; 70 | uint32_t key_4; 71 | uint32_t key_5; 72 | uint32_t unknown_3; 73 | uint32_t unknown_4; 74 | uint32_t offset_data_start; 75 | char magic[0x0C]; 76 | } pmlxzj_footer_t; 77 | 78 | typedef struct { 79 | uint32_t field_0; 80 | uint32_t field_4; 81 | uint32_t field_8; 82 | int32_t total_frame_count; 83 | uint32_t field_10; 84 | uint32_t field_14; 85 | uint32_t field_18; 86 | uint32_t field_1C; 87 | uint32_t field_20; 88 | uint32_t field_24; // some kind of special flag... 89 | } pmlxzj_config_14d8_t; 90 | 91 | typedef struct { 92 | uint8_t lic_data_1[20]; 93 | uint8_t lic_data_2[20]; 94 | uint8_t user_watermark[40]; 95 | } pmlxzj_watermark_t; 96 | 97 | typedef struct { 98 | uint16_t wFormatTag; 99 | uint16_t nChannels; 100 | uint32_t nSamplesPerSec; 101 | uint32_t nAvgBytesPerSec; 102 | uint16_t nBlockAlign; 103 | uint16_t wBitsPerSample; 104 | uint16_t cbSize; 105 | } pmlxzj_wave_format_ex; 106 | #pragma pack(pop) 107 | 108 | typedef struct { 109 | uint8_t profile_id; 110 | uint8_t sample_rate_id; 111 | uint8_t channels_id; 112 | } pmlxzj_aac_audio_config_t; 113 | 114 | typedef struct { 115 | long offset; 116 | uint32_t size; 117 | uint32_t segment_count; 118 | 119 | // decoder specific data 120 | uint8_t format[4]; 121 | uint32_t* segment_sizes; 122 | pmlxzj_aac_audio_config_t config; 123 | } pmlxzj_audio_aac_t; 124 | 125 | typedef struct { 126 | long offset; 127 | uint32_t size; 128 | } pmlxzj_audio_wav_t; 129 | 130 | typedef struct { 131 | long offset; 132 | uint32_t segment_count; 133 | } pmlxzj_audio_wav_zlib_t; 134 | 135 | typedef struct { 136 | long offset; 137 | uint32_t size; 138 | uint32_t segment_count; 139 | uint32_t* offsets; 140 | } pmlxzj_audio_mp3_t; 141 | 142 | typedef struct { 143 | FILE* file; 144 | 145 | long file_size; 146 | // encrypt_mode: 0(no_encryption), 1(EditLock), 2(PlayLock) 147 | int encrypt_mode; 148 | char nonce_buffer[20]; 149 | pmlxzj_footer_t footer; 150 | uint32_t* idx1; 151 | size_t idx1_count; 152 | uint32_t* idx2; 153 | size_t idx2_count; 154 | 155 | pmlxzj_config_14d8_t field_14d8; 156 | 157 | long watermark_offset; 158 | pmlxzj_watermark_t watermark; 159 | 160 | long frame_metadata_offset; 161 | long first_frame_offset; 162 | long frame; 163 | 164 | // Audio metadata 165 | int audio_metadata_version; 166 | long audio_metadata_offset; 167 | 168 | union { 169 | pmlxzj_audio_mp3_t mp3; 170 | pmlxzj_audio_wav_t wav; 171 | pmlxzj_audio_wav_zlib_t wav_zlib; 172 | pmlxzj_audio_aac_t aac; 173 | } audio; 174 | } pmlxzj_state_t; 175 | 176 | typedef struct { 177 | char password[20]; 178 | FILE* input_file; 179 | bool verbose; 180 | bool resume_on_bad_password; 181 | } pmlxzj_user_params_t; 182 | 183 | typedef enum { 184 | PMLXZJ_OK = 0, 185 | PMLXZJ_MAGIC_NOT_MATCH = 1, 186 | /** @deprecated */ 187 | PMLXZJ_UNSUPPORTED_MODE_2 = 2, 188 | PMLXZJ_ALLOCATE_INDEX_LIST_ERROR = 3, 189 | PMLXZJ_SCAN_U32_ARRAY_ALLOC_ERROR = 4, 190 | PMLXZJ_AUDIO_OFFSET_UNSUPPORTED = 5, 191 | PMLXZJ_AUDIO_TYPE_UNSUPPORTED = 6, 192 | PMLXZJ_PASSWORD_ERROR = 7, 193 | PMLXZJ_AUDIO_INCORRECT_TYPE = 8, 194 | PMLXZJ_AUDIO_NOT_PRESENT = 9, 195 | PMLXZJ_GZIP_BUFFER_ALLOC_FAILURE = 10, 196 | PMLXZJ_GZIP_BUFFER_TOO_LARGE = 11, 197 | PMLXZJ_GZIP_INFLATE_FAILURE = 12, 198 | PMLXZJ_NO_AUDIO = 13, 199 | PMLXZJ_AUDIO_AAC_DECODER_INFO_TOO_SMALL = 14, 200 | PMLXZJ_AUDIO_AAC_DECODER_UNSUPPORTED_SAMPLE_RATE = 15, 201 | PMLXZJ_AUDIO_AAC_INVALID_DECODER_SPECIFIC_INFO = 16, 202 | PMLXZJ_AUDIO_AAC_INVALID_FRAME_SIZE = 17, 203 | } pmlxzj_state_e; 204 | 205 | typedef enum { 206 | PMLXZJ_ENUM_CONTINUE = 0, 207 | PMLXZJ_ENUM_ABORT = 1, 208 | } pmlxzj_enumerate_state_e; 209 | 210 | typedef struct { 211 | // might be wrong 212 | uint32_t left; 213 | uint32_t top; 214 | uint32_t right; 215 | uint32_t bottom; 216 | } pmlxzj_rect; 217 | 218 | typedef struct { 219 | int32_t frame_id; 220 | int32_t image_id; 221 | pmlxzj_rect cord; // image position. all zero = full canvas size or missing 222 | uint32_t compressed_size; 223 | uint32_t decompressed_size; 224 | } pmlxzj_frame_info_t; 225 | typedef pmlxzj_enumerate_state_e(pmlxzj_enumerate_callback_t)(pmlxzj_state_t* ctx, 226 | pmlxzj_frame_info_t* frame, 227 | void* extra_callback_data); 228 | 229 | uint32_t pmlxzj_password_checksum(const char* password); 230 | pmlxzj_state_e pmlxzj_init(pmlxzj_state_t* ctx, pmlxzj_user_params_t* params); 231 | pmlxzj_state_e pmlxzj_init_audio(pmlxzj_state_t* ctx); 232 | pmlxzj_state_e pmlxzj_init_frame(pmlxzj_state_t* ctx); 233 | pmlxzj_state_e pmlxzj_init_all(pmlxzj_state_t* ctx, pmlxzj_user_params_t* params); 234 | 235 | // Frames / images 236 | pmlxzj_enumerate_state_e pmlxzj_enumerate_images(pmlxzj_state_t* ctx, 237 | pmlxzj_enumerate_callback_t* callback, 238 | void* extra_callback_data); 239 | // Audio 240 | pmlxzj_state_e pmlxzj_audio_dump_to_file(pmlxzj_state_t* ctx, FILE* f_audio); 241 | 242 | pmlxzj_state_e pmlxzj_audio_dump_raw_wave(pmlxzj_state_t* ctx, FILE* f_audio); 243 | pmlxzj_state_e pmlxzj_audio_dump_compressed_wave(pmlxzj_state_t* ctx, FILE* f_audio); 244 | pmlxzj_state_e pmlxzj_audio_dump_mp3(pmlxzj_state_t* ctx, FILE* f_audio); 245 | pmlxzj_state_e pmlxzj_audio_dump_aac(pmlxzj_state_t* ctx, FILE* f_audio); 246 | -------------------------------------------------------------------------------- /pmlxzj_audio.c: -------------------------------------------------------------------------------- 1 | #include "pmlxzj.h" 2 | #include "pmlxzj_audio_aac.h" 3 | #include "pmlxzj_enum_names.h" 4 | #include "pmlxzj_utils.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #define MAX_COMPRESSED_CHUNK_SIZE (0x1E848) 13 | #define ZLIB_INFLATE_BUFFER_SIZE (4096) 14 | 15 | pmlxzj_state_e pmlxzj_init_audio(pmlxzj_state_t* ctx) { 16 | if (ctx->audio_metadata_offset == 0) { 17 | // no audio 18 | return PMLXZJ_NO_AUDIO; 19 | } 20 | 21 | FILE* f_src = ctx->file; 22 | switch (ctx->footer.config.audio_codec) { 23 | case PMLXZJ_AUDIO_TYPE_WAVE_COMPRESSED: { 24 | pmlxzj_audio_wav_zlib_t* audio = &ctx->audio.wav_zlib; 25 | 26 | fseek(f_src, ctx->audio_metadata_offset, SEEK_SET); 27 | fread(&audio->segment_count, sizeof(audio->segment_count), 1, f_src); 28 | audio->offset = ftell(f_src); 29 | break; 30 | } 31 | 32 | case PMLXZJ_AUDIO_TYPE_WAVE_RAW: { 33 | pmlxzj_audio_wav_t* audio = &ctx->audio.wav; 34 | 35 | fseek(f_src, ctx->audio_metadata_offset, SEEK_SET); 36 | fread(&audio->size, sizeof(audio->size), 1, f_src); 37 | audio->offset = ftell(f_src); 38 | break; 39 | } 40 | 41 | case PMLXZJ_AUDIO_TYPE_LOSSY_MP3: { 42 | pmlxzj_audio_mp3_t* audio = &ctx->audio.mp3; 43 | 44 | assert(sizeof(pmlxzj_wave_format_ex) == 0x12); 45 | pmlxzj_wave_format_ex wav_spec; 46 | pmlxzj_wave_format_ex mp3_spec; 47 | uint32_t unk_audio_prop; 48 | uint32_t mp3_chunk_count; 49 | 50 | fseek(f_src, ctx->audio_metadata_offset, SEEK_SET); 51 | fread(&wav_spec, sizeof(wav_spec), 1, f_src); 52 | fread(&mp3_spec, sizeof(mp3_spec), 1, f_src); 53 | fread(&unk_audio_prop, sizeof(unk_audio_prop), 1, f_src); 54 | fread(&mp3_chunk_count, sizeof(mp3_chunk_count), 1, f_src); 55 | if (mp3_chunk_count == 0) { 56 | break; 57 | } 58 | 59 | audio->segment_count = mp3_chunk_count + 1; 60 | audio->offsets = calloc(audio->segment_count, sizeof(uint32_t)); 61 | fread(audio->offsets, sizeof(uint32_t), audio->segment_count, f_src); 62 | fread(&audio->size, sizeof(audio->size), 1, f_src); 63 | assert(audio->offsets[0] == 0); 64 | audio->offsets[mp3_chunk_count] = audio->size; 65 | audio->offset = ftell(f_src); 66 | break; 67 | } 68 | 69 | case PMLXZJ_AUDIO_TYPE_LOSSY_AAC: { 70 | pmlxzj_audio_aac_t* audio = &ctx->audio.aac; 71 | 72 | fseek(f_src, ctx->audio_metadata_offset, SEEK_SET); 73 | 74 | pmlxzj_wave_format_ex wav_spec; 75 | uint32_t unused_field_80; 76 | 77 | fread(&wav_spec, sizeof(wav_spec), 1, f_src); 78 | fread(&unused_field_80, sizeof(unused_field_80), 1, f_src); 79 | 80 | uint32_t format_len; 81 | fread(&format_len, sizeof(format_len), 1, f_src); 82 | if (format_len > sizeof(audio->format)) { 83 | return PMLXZJ_AUDIO_AAC_INVALID_DECODER_SPECIFIC_INFO; 84 | } 85 | fread(&audio->format, format_len, 1, f_src); 86 | pmlxzj_state_e state = pmlxzj_aac_parse_decoder_specific_info(&audio->config, audio->format, format_len); 87 | if (state != PMLXZJ_OK) { 88 | return state; 89 | } 90 | 91 | uint32_t segment_count_in_bytes; 92 | fread(&segment_count_in_bytes, sizeof(segment_count_in_bytes), 1, f_src); 93 | audio->segment_sizes = malloc(segment_count_in_bytes); 94 | audio->segment_count = segment_count_in_bytes / sizeof(uint32_t); 95 | fread(audio->segment_sizes, 1, segment_count_in_bytes, f_src); 96 | 97 | for (uint32_t i = 0; i < audio->segment_count; i++) { 98 | if (audio->segment_sizes[i] > PMLXZJ_AAC_MAX_FRAME_DATA_LEN) { 99 | free(audio->segment_sizes); 100 | audio->segment_sizes = NULL; 101 | return PMLXZJ_AUDIO_AAC_INVALID_FRAME_SIZE; 102 | } 103 | } 104 | 105 | fread(&audio->size, sizeof(audio->size), 1, f_src); 106 | audio->offset = ftell(f_src); 107 | } 108 | } 109 | 110 | return PMLXZJ_OK; 111 | } 112 | 113 | bool inflate_chunk(FILE* output, uint8_t* input, size_t length) { 114 | z_stream strm = {0}; 115 | strm.next_in = (Bytef*)input; 116 | strm.avail_in = (uInt)length; 117 | 118 | if (inflateInit(&strm) != Z_OK) { 119 | fprintf(stderr, "Failed to initialize inflate\n"); 120 | return false; 121 | } 122 | 123 | bool ok = true; 124 | char buffer[ZLIB_INFLATE_BUFFER_SIZE]; 125 | 126 | do { 127 | strm.next_out = (Bytef*)buffer; 128 | strm.avail_out = ZLIB_INFLATE_BUFFER_SIZE; 129 | 130 | int ret = inflate(&strm, Z_NO_FLUSH); 131 | 132 | if (ret == Z_STREAM_ERROR) { 133 | fprintf(stderr, "Zlib stream error\n"); 134 | ok = false; 135 | break; 136 | } 137 | 138 | if (ret == Z_NEED_DICT || ret == Z_DATA_ERROR || ret == Z_MEM_ERROR) { 139 | fprintf(stderr, "Zlib inflate error %s\n", strm.msg); 140 | ok = false; 141 | break; 142 | } 143 | 144 | size_t output_length = ZLIB_INFLATE_BUFFER_SIZE - strm.avail_out; 145 | fwrite(buffer, 1, output_length, output); 146 | } while (strm.avail_out == 0); 147 | 148 | inflateEnd(&strm); 149 | return ok; 150 | } 151 | 152 | pmlxzj_state_e pmlxzj_audio_dump_to_file(pmlxzj_state_t* ctx, FILE* f_audio) { 153 | if (ctx->audio_metadata_offset == 0) { 154 | fprintf(stderr, "File does not contain audio or not initialized.\n"); 155 | return PMLXZJ_AUDIO_NOT_PRESENT; 156 | } 157 | 158 | pmlxzj_state_e result = PMLXZJ_AUDIO_TYPE_UNSUPPORTED; 159 | switch (ctx->footer.config.audio_codec) { 160 | case PMLXZJ_AUDIO_TYPE_LOSSY_MP3: 161 | result = pmlxzj_audio_dump_mp3(ctx, f_audio); 162 | break; 163 | 164 | case PMLXZJ_AUDIO_TYPE_WAVE_COMPRESSED: 165 | result = pmlxzj_audio_dump_compressed_wave(ctx, f_audio); 166 | break; 167 | 168 | case PMLXZJ_AUDIO_TYPE_WAVE_RAW: 169 | result = pmlxzj_audio_dump_raw_wave(ctx, f_audio); 170 | break; 171 | 172 | case PMLXZJ_AUDIO_TYPE_LOSSY_AAC: 173 | result = pmlxzj_audio_dump_aac(ctx, f_audio); 174 | break; 175 | 176 | default: 177 | break; 178 | } 179 | 180 | if (result != PMLXZJ_OK) { 181 | printf("ERROR: %s: failed to inflate audio: %d (%s)", __func__, result, pmlxzj_get_state_name(result)); 182 | } 183 | return result; 184 | } 185 | 186 | pmlxzj_state_e pmlxzj_audio_dump_raw_wave(pmlxzj_state_t* ctx, FILE* f_audio) { 187 | if (ctx->footer.config.audio_codec != PMLXZJ_AUDIO_TYPE_WAVE_RAW) { 188 | return PMLXZJ_AUDIO_INCORRECT_TYPE; 189 | } 190 | 191 | pmlxzj_audio_wav_t* audio = &ctx->audio.wav; 192 | 193 | FILE* f_src = ctx->file; 194 | fseek(f_src, audio->offset, SEEK_SET); 195 | pmlxzj_util_copy(f_audio, ctx->file, audio->size); 196 | return PMLXZJ_OK; 197 | } 198 | 199 | pmlxzj_state_e pmlxzj_audio_dump_compressed_wave(pmlxzj_state_t* ctx, FILE* f_audio) { 200 | if (ctx->footer.config.audio_codec != PMLXZJ_AUDIO_TYPE_WAVE_COMPRESSED) { 201 | return PMLXZJ_AUDIO_INCORRECT_TYPE; 202 | } 203 | 204 | pmlxzj_audio_wav_zlib_t* audio = &ctx->audio.wav_zlib; 205 | 206 | FILE* f_src = ctx->file; 207 | uint32_t len = audio->segment_count; 208 | fseek(f_src, audio->offset, SEEK_SET); 209 | 210 | pmlxzj_state_e result = PMLXZJ_OK; 211 | uint8_t buffer[MAX_COMPRESSED_CHUNK_SIZE] = {0}; 212 | for (uint32_t i = 0; i < len; i++) { 213 | uint32_t chunk_size = 0; 214 | fread(&chunk_size, sizeof(chunk_size), 1, f_src); 215 | if (chunk_size > MAX_COMPRESSED_CHUNK_SIZE) { 216 | fprintf(stderr, "compressed audio chunk size too large (chunk=%d, offset=%ld)\n", i, ftell(f_src)); 217 | result = PMLXZJ_GZIP_BUFFER_TOO_LARGE; 218 | break; 219 | } 220 | fread(buffer, chunk_size, 1, f_src); 221 | if (!inflate_chunk(f_audio, buffer, chunk_size)) { 222 | result = PMLXZJ_GZIP_INFLATE_FAILURE; 223 | break; 224 | } 225 | } 226 | return result; 227 | } 228 | 229 | pmlxzj_state_e pmlxzj_audio_dump_mp3(pmlxzj_state_t* ctx, FILE* f_audio) { 230 | if (ctx->footer.config.audio_codec != PMLXZJ_AUDIO_TYPE_LOSSY_MP3) { 231 | return PMLXZJ_AUDIO_INCORRECT_TYPE; 232 | } 233 | 234 | pmlxzj_audio_mp3_t* audio = &ctx->audio.mp3; 235 | 236 | fseek(ctx->file, audio->offset, SEEK_SET); 237 | pmlxzj_util_copy(f_audio, ctx->file, audio->size); 238 | return PMLXZJ_OK; 239 | } 240 | 241 | pmlxzj_state_e pmlxzj_audio_dump_aac(pmlxzj_state_t* ctx, FILE* f_audio) { 242 | if (ctx->footer.config.audio_codec != PMLXZJ_AUDIO_TYPE_LOSSY_AAC) { 243 | return PMLXZJ_AUDIO_INCORRECT_TYPE; 244 | } 245 | 246 | pmlxzj_audio_aac_t* audio = &ctx->audio.aac; 247 | fseek(ctx->file, audio->offset, SEEK_SET); 248 | uint8_t buffer[PMLXZJ_AAC_ADTS_HEADER_LEN + PMLXZJ_AAC_MAX_FRAME_DATA_LEN] = {0}; 249 | 250 | uint32_t n = audio->segment_count; 251 | for (uint32_t i = 0; i < n; i++) { 252 | uint32_t chunk_size = audio->segment_sizes[i]; 253 | assert(chunk_size < PMLXZJ_AAC_MAX_FRAME_DATA_LEN); 254 | 255 | size_t header_len = pmlxzj_aac_adts_header(buffer, &audio->config, chunk_size); 256 | assert(header_len == PMLXZJ_AAC_ADTS_HEADER_LEN); 257 | 258 | size_t bytes_read = fread(&buffer[header_len], 1, chunk_size, ctx->file); 259 | assert(bytes_read == chunk_size); 260 | 261 | fwrite(buffer, 1, header_len + bytes_read, f_audio); 262 | } 263 | assert(ftell(ctx->file) == (long)(audio->offset + audio->size)); 264 | 265 | return PMLXZJ_OK; 266 | } 267 | -------------------------------------------------------------------------------- /pmlxzj_audio_aac.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "pmlxzj.h" 4 | 5 | #include 6 | #include 7 | 8 | #define PMLXZJ_COUNT_OF(arr) (sizeof(arr) / sizeof(arr[0])) 9 | #define PMLXZJ_AAC_ADTS_HEADER_LEN (7) 10 | #define PMLXZJ_AAC_MASK_BY_BITS(val, n) ((val) & ((1 << n) - 1)) 11 | #define PMLXZJ_AAC_MAX_FRAME_DATA_LEN ((1 << 13) - 1 - PMLXZJ_AAC_ADTS_HEADER_LEN) 12 | 13 | inline static const char* pmlxzj_adts_get_profile_name(uint8_t profile) { 14 | static const char* names[4] = {"aac-main", "aac-lc", "aac-ssr", "aac-he?"}; 15 | if (profile < PMLXZJ_COUNT_OF(names)) { 16 | return names[profile]; 17 | } 18 | return "unknown"; 19 | } 20 | 21 | inline static uint32_t pmlxzj_adts_get_sample_rate(uint8_t sample_rate_idx) { 22 | static const uint32_t sample_rates[] = { 23 | 96000, 88200, 64000, 48000, 44100, 32000, 24 | 24000, 22050, 16000, 12000, 11025, 8000 25 | }; 26 | 27 | if (sample_rate_idx < PMLXZJ_COUNT_OF(sample_rates)) { 28 | return sample_rates[sample_rate_idx]; 29 | } 30 | 31 | return 0; 32 | } 33 | 34 | inline static uint8_t pmlxzj_aac_map_profile_to_adts(uint8_t profile) { 35 | switch (profile) { 36 | case 1: // AAC Main 37 | return 0; 38 | case 2: // AAC LC 39 | return 1; 40 | case 3: // AAC SSR 41 | return 2; 42 | default: // (5) SBR or other: HE? 43 | return 3; 44 | } 45 | } 46 | inline static uint8_t pmlxzj_aac_map_sample_rate_idx(uint8_t sample_rate_idx) { 47 | // Sample rate index is (0..11) inclusive. 48 | if (sample_rate_idx < 12) { 49 | return sample_rate_idx; 50 | } 51 | 52 | return 0xFF; 53 | } 54 | 55 | inline static pmlxzj_state_e pmlxzj_aac_parse_decoder_specific_info(pmlxzj_aac_audio_config_t* result, 56 | const uint8_t* buf, 57 | size_t len) { 58 | if (len < 2) { 59 | return PMLXZJ_AUDIO_AAC_DECODER_INFO_TOO_SMALL; 60 | } 61 | 62 | const uint8_t profile = PMLXZJ_AAC_MASK_BY_BITS(buf[0] >> 3, 5); 63 | const uint8_t sample_rate = PMLXZJ_AAC_MASK_BY_BITS((buf[0] << 1) | (buf[1] >> 7), 4); 64 | const uint8_t channels = PMLXZJ_AAC_MASK_BY_BITS(buf[1] >> 3, 4); 65 | 66 | if (sample_rate == 0xFF) { 67 | return PMLXZJ_AUDIO_AAC_DECODER_UNSUPPORTED_SAMPLE_RATE; 68 | } 69 | 70 | result->profile_id = pmlxzj_aac_map_profile_to_adts(profile); 71 | result->sample_rate_id = sample_rate; 72 | result->channels_id = channels; 73 | return PMLXZJ_OK; 74 | } 75 | 76 | // header should have size of 9 or more to be safe. 77 | // returns the number of bytes required by the header. 78 | // `frame_data_len` should be `PMLXZJ_AAC_MAX_FRAME_DATA_LEN` or lower. 79 | inline static size_t pmlxzj_aac_adts_header(uint8_t* header, 80 | const pmlxzj_aac_audio_config_t* config, 81 | uint16_t frame_data_len) { 82 | header[0] = 0xff; // sync word (12-bits) 83 | 84 | // sync word: 4-bits 85 | const uint8_t mpeg_version = PMLXZJ_AAC_MASK_BY_BITS(1, 1); // 1-bit 86 | const uint8_t layer = PMLXZJ_AAC_MASK_BY_BITS(0, 2); // 2-bits 87 | const uint8_t protection_absence = PMLXZJ_AAC_MASK_BY_BITS(1, 1); // 1-bit 88 | header[1] = 0xf0 | (mpeg_version << 3) | (layer << 1) | protection_absence; 89 | 90 | const uint8_t profile = config->profile_id; // 2-bits 91 | const uint8_t sample_rate_idx_mapped = PMLXZJ_AAC_MASK_BY_BITS(config->sample_rate_id, 4); // 4-bits 92 | const uint8_t private_bit = 0; // 1-bit 93 | const uint8_t channels_idx = PMLXZJ_AAC_MASK_BY_BITS(config->channels_id, 3); // 3-bits (1 + 2) 94 | header[2] = (profile << 6) | (sample_rate_idx_mapped << 2) | (private_bit << 1) | (channels_idx >> 2); 95 | 96 | // channel (2-bits) 97 | const uint8_t originality = PMLXZJ_AAC_MASK_BY_BITS(0, 1); // 1-bit 98 | const uint8_t home_usage = PMLXZJ_AAC_MASK_BY_BITS(0, 1); // 1-bit 99 | const uint8_t copyrighted = PMLXZJ_AAC_MASK_BY_BITS(0, 1); // 1-bit 100 | const uint8_t copyright_start = PMLXZJ_AAC_MASK_BY_BITS(0, 1); // 1-bit 101 | const uint16_t frame_size = 102 | PMLXZJ_AAC_MASK_BY_BITS(frame_data_len + PMLXZJ_AAC_ADTS_HEADER_LEN, 13); // 13-bits (2 + 8 + 3) 103 | header[3] = (channels_idx << 6) | (originality << 5) | (home_usage << 4) // 104 | | (copyrighted << 3) | (copyright_start << 2) | (frame_size >> 11); 105 | header[4] = frame_size >> 3; 106 | 107 | // frame_size (3-bits) 108 | const uint16_t buffer_fullness = PMLXZJ_AAC_MASK_BY_BITS(0x7FF, 11); // 11-bits (5 + 6) 109 | header[5] = (uint8_t)((frame_size << 5) | (buffer_fullness >> 6)); 110 | 111 | // buffer_fullness (6-bits) 112 | const uint8_t number_of_aac_frames = PMLXZJ_AAC_MASK_BY_BITS(0, 2); // 2-bits 113 | header[6] = (uint8_t)(buffer_fullness << 2) | (number_of_aac_frames); 114 | 115 | return 7; 116 | } 117 | -------------------------------------------------------------------------------- /pmlxzj_commands.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | int pmlxzj_cmd_disable_audio(int argc, char** argv); 6 | int pmlxzj_cmd_extract_audio(int argc, char** argv); 7 | int pmlxzj_cmd_print_info(int argc, char** argv); 8 | int pmlxzj_cmd_unlock_exe(int argc, char** argv); 9 | int pmlxzj_cmd_remove_watermark(int argc, char** argv); 10 | 11 | static inline void pmlxzj_usage(char* argv0) { 12 | char* name = basename(argv0); 13 | printf("Usage:\n\n"); 14 | printf(" %s audio-dump \n", name); 15 | printf(" %s audio-disable \n", name); 16 | printf("\n"); 17 | printf(" %s remove-watermark [-v] [-r] [-t] \n", name); 18 | printf(" -r Remove 'unregistered' watermark.\n"); 19 | printf(" -t Remove playback text watermark.\n"); 20 | printf(" -v Verbose logging.\n"); 21 | printf("\n"); 22 | printf(" %s unlock [-v] [-r] [-p password] [-P password.txt] \n", name); 23 | printf(" -r Resume unlock, even when password checksum mismatch.\n"); 24 | printf(" -p Password.\n"); 25 | printf(" -P Password, but read from a text file instead.\n"); 26 | printf(" -v Verbose logging.\n"); 27 | printf("\n"); 28 | printf(" %s info [-v] [-i] [-f] \n", name); 29 | printf(" -v Verbose logging.\n"); 30 | printf(" -i Print indexes (unknown meaning).\n"); 31 | printf(" -v Print frames in the player data.\n"); 32 | } 33 | -------------------------------------------------------------------------------- /pmlxzj_enum_names.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "pmlxzj.h" 3 | 4 | static inline const char* pmlxzj_get_audio_codec_name(pmlxzj_audio_codec_e audio_codec) { 5 | switch (audio_codec) { 6 | case PMLXZJ_AUDIO_TYPE_WAVE_RAW: 7 | return "PMLXZJ_AUDIO_TYPE_WAVE_RAW"; 8 | case PMLXZJ_AUDIO_TYPE_WAVE_COMPRESSED: 9 | return "PMLXZJ_AUDIO_TYPE_WAVE_COMPRESSED"; 10 | case PMLXZJ_AUDIO_TYPE_LOSSY_MP3: 11 | return "PMLXZJ_AUDIO_TYPE_LOSSY_MP3"; 12 | case PMLXZJ_AUDIO_TYPE_LOSSY_TRUE_SPEECH: 13 | return "PMLXZJ_AUDIO_TYPE_LOSSY_TRUE_SPEECH"; 14 | case PMLXZJ_AUDIO_TYPE_LOSSY_AAC: 15 | return "PMLXZJ_AUDIO_TYPE_LOSSY_AAC"; 16 | default: 17 | return "UNKNOWN"; 18 | } 19 | } 20 | 21 | static inline const char* pmlxzj_get_state_name(pmlxzj_state_e state) { 22 | switch (state) { 23 | case PMLXZJ_OK: 24 | return "PMLXZJ_OK"; 25 | case PMLXZJ_MAGIC_NOT_MATCH: 26 | return "PMLXZJ_MAGIC_NOT_MATCH"; 27 | case PMLXZJ_UNSUPPORTED_MODE_2: 28 | return "PMLXZJ_UNSUPPORTED_MODE_2"; 29 | case PMLXZJ_ALLOCATE_INDEX_LIST_ERROR: 30 | return "PMLXZJ_ALLOCATE_INDEX_LIST_ERROR"; 31 | case PMLXZJ_SCAN_U32_ARRAY_ALLOC_ERROR: 32 | return "PMLXZJ_SCAN_U32_ARRAY_ALLOC_ERROR"; 33 | case PMLXZJ_AUDIO_OFFSET_UNSUPPORTED: 34 | return "PMLXZJ_AUDIO_OFFSET_UNSUPPORTED"; 35 | case PMLXZJ_AUDIO_TYPE_UNSUPPORTED: 36 | return "PMLXZJ_AUDIO_TYPE_UNSUPPORTED"; 37 | case PMLXZJ_PASSWORD_ERROR: 38 | return "PMLXZJ_PASSWORD_ERROR"; 39 | case PMLXZJ_AUDIO_INCORRECT_TYPE: 40 | return "PMLXZJ_AUDIO_INCORRECT_TYPE"; 41 | case PMLXZJ_AUDIO_NOT_PRESENT: 42 | return "PMLXZJ_AUDIO_NOT_PRESENT"; 43 | case PMLXZJ_GZIP_BUFFER_ALLOC_FAILURE: 44 | return "PMLXZJ_GZIP_BUFFER_ALLOC_FAILURE"; 45 | case PMLXZJ_GZIP_BUFFER_TOO_LARGE: 46 | return "PMLXZJ_GZIP_BUFFER_TOO_LARGE"; 47 | case PMLXZJ_GZIP_INFLATE_FAILURE: 48 | return "PMLXZJ_GZIP_INFLATE_FAILURE"; 49 | case PMLXZJ_NO_AUDIO: 50 | return "PMLXZJ_NO_AUDIO"; 51 | case PMLXZJ_AUDIO_AAC_DECODER_INFO_TOO_SMALL: 52 | return "PMLXZJ_AUDIO_AAC_DECODER_INFO_TOO_SMALL"; 53 | case PMLXZJ_AUDIO_AAC_DECODER_UNSUPPORTED_SAMPLE_RATE: 54 | return "PMLXZJ_AUDIO_AAC_DECODER_UNSUPPORTED_SAMPLE_RATE"; 55 | case PMLXZJ_AUDIO_AAC_INVALID_DECODER_SPECIFIC_INFO: 56 | return "PMLXZJ_AUDIO_AAC_INVALID_DECODER_SPECIFIC_INFO"; 57 | case PMLXZJ_AUDIO_AAC_INVALID_FRAME_SIZE: 58 | return "PMLXZJ_AUDIO_AAC_INVALID_FRAME_SIZE"; 59 | default: 60 | return "UNKNOWN"; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /pmlxzj_frame.c: -------------------------------------------------------------------------------- 1 | #include "pmlxzj.h" 2 | #include "pmlxzj_utils.h" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | void skip_sized_packet(FILE* f) { 10 | uint32_t skip = 0; 11 | fread(&skip, sizeof(skip), 1, f); 12 | if (skip != 0) { 13 | fseek(f, (long)skip, SEEK_CUR); 14 | } 15 | } 16 | 17 | pmlxzj_state_e pmlxzj_init_frame(pmlxzj_state_t* ctx) { 18 | FILE* f = ctx->file; 19 | 20 | // init code from: TPlayForm_repareplay2 21 | fseek(f, (long)ctx->frame_metadata_offset, SEEK_SET); 22 | fread(&ctx->field_14d8, sizeof(ctx->field_14d8), 1, f); 23 | 24 | bool f24 = ctx->field_14d8.field_24 == 1; 25 | if (!f24) { 26 | fprintf(stderr, "WARN: ctx->field_14d8.field_24 == %d. Frame init may fail.\n", ctx->field_14d8.field_24); 27 | } 28 | 29 | fread(&ctx->watermark, sizeof(ctx->watermark), 1, f); 30 | 31 | const long seek_unused_data = 4 * 3 /* field_1484/field_1488/field_1490 */ + 20 /* font: "微软雅黑" */ 32 | + 4 /* field_1498 */ + 4 /* off_149C_bitmask */; 33 | 34 | #ifndef NDEBUG 35 | { 36 | printf("DEBUG (unused data):\n"); 37 | char buff[seek_unused_data]; 38 | fread(buff, seek_unused_data, 1, f); 39 | pmlxzj_util_hexdump(buff, seek_unused_data); 40 | } 41 | #else 42 | fseek(f, seek_unused_data, SEEK_CUR); 43 | #endif 44 | 45 | if (f24) { 46 | fseek(f, 8, SEEK_CUR); 47 | skip_sized_packet(f); // stream2 setup 48 | } 49 | 50 | ctx->first_frame_offset = ftell(f); 51 | #ifndef NDEBUG 52 | { 53 | printf("DEBUG (test first frame):\n"); 54 | struct __attribute__((__packed__)) { 55 | uint32_t compressed; 56 | uint32_t decompressed; 57 | char data_hdr[2]; 58 | } test_frame_hdr; 59 | fread(&test_frame_hdr, sizeof(test_frame_hdr), 1, f); 60 | pmlxzj_util_hexdump(&test_frame_hdr, sizeof(test_frame_hdr)); 61 | assert(memcmp(test_frame_hdr.data_hdr, "\x78\x9C", 2) == 0 && "invalid zlib data header"); 62 | } 63 | #endif 64 | 65 | return PMLXZJ_OK; 66 | } 67 | 68 | pmlxzj_enumerate_state_e pmlxzj_enumerate_images(pmlxzj_state_t* ctx, 69 | pmlxzj_enumerate_callback_t* callback, 70 | void* extra_callback_data) { 71 | pmlxzj_enumerate_state_e enum_state = PMLXZJ_ENUM_CONTINUE; 72 | FILE* file = ctx->file; 73 | bool field_24_set = ctx->field_14d8.field_24 == 1; 74 | fseek(file, ctx->first_frame_offset, SEEK_SET); 75 | pmlxzj_frame_info_t frame_info = {0}; 76 | 77 | // process first frame 78 | { 79 | fread(&frame_info.compressed_size, sizeof(frame_info.compressed_size), 1, file); 80 | long pos = ftell(file); 81 | fread(&frame_info.decompressed_size, sizeof(frame_info.decompressed_size), 1, file); 82 | fseek(file, -4, SEEK_CUR); 83 | enum_state = callback(ctx, &frame_info, extra_callback_data); 84 | frame_info.image_id++; 85 | 86 | fseek(file, pos + (long)frame_info.compressed_size, SEEK_SET); 87 | fread(&frame_info.frame_id, sizeof(frame_info.frame_id), 1, file); 88 | assert(frame_info.frame_id == 0); 89 | if (field_24_set) { 90 | fseek(file, 8, SEEK_CUR); // f24: timestamps? 91 | } 92 | } 93 | 94 | int32_t last_frame_id = -(ctx->field_14d8.total_frame_count - 1); 95 | while (enum_state == PMLXZJ_ENUM_CONTINUE && last_frame_id != frame_info.frame_id) { 96 | // seek stream2 97 | if (field_24_set) { 98 | uint32_t stream2_len = 0; 99 | fread(&stream2_len, sizeof(stream2_len), 1, file); 100 | if (stream2_len != 0) { 101 | fseek(file, (long)stream2_len, SEEK_CUR); 102 | } 103 | } 104 | 105 | // Read frame id. If `frame_id` is negative, the frame uses previous content from previous frame. 106 | fread(&frame_info.frame_id, sizeof(frame_info.frame_id), 1, file); 107 | while (enum_state == PMLXZJ_ENUM_CONTINUE && frame_info.frame_id > 0) { 108 | int32_t eof_mark; 109 | 110 | // read cords 111 | fread(&frame_info.cord, sizeof(frame_info.cord), 1, file); 112 | 113 | // read compressed size and decompressed size, and a special eof mark (not used?) 114 | fread(&frame_info.compressed_size, sizeof(frame_info.compressed_size), 1, file); 115 | long pos = ftell(file); 116 | fread(&frame_info.decompressed_size, sizeof(frame_info.decompressed_size), 1, file); 117 | fread(&eof_mark, sizeof(eof_mark), 1, file); 118 | 119 | if ((int32_t)frame_info.decompressed_size == -1 && eof_mark == -1) { 120 | fprintf(stderr, "WARN: unknown handling of frame, skip"); 121 | } else { 122 | fseek(file, -8, SEEK_CUR); 123 | enum_state = callback(ctx, &frame_info, extra_callback_data); 124 | } 125 | frame_info.image_id++; 126 | fseek(file, pos + (long)frame_info.compressed_size, SEEK_SET); 127 | fread(&frame_info.frame_id, sizeof(frame_info.frame_id), 1, file); // read next frame id. 128 | } 129 | 130 | if (field_24_set) { 131 | fseek(file, 8, SEEK_CUR); 132 | } 133 | } 134 | 135 | return enum_state; 136 | } 137 | -------------------------------------------------------------------------------- /pmlxzj_utils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #define PMLXZJ_MIN(a, b) (((a) < (b)) ? (a) : (b)) 10 | #define PMLXZJ_UNUSED_PARAMETER(x) (void)(x) 11 | 12 | static inline void pmlxzj_util_hexdump(void* ptr, int buflen) { 13 | unsigned char* buf = (unsigned char*)ptr; 14 | int i, j; 15 | for (i = 0; i < buflen; i += 16) { 16 | printf("%06x: ", i); 17 | for (j = 0; j < 16; j++) 18 | if (i + j < buflen) 19 | printf("%02x ", buf[i + j]); 20 | else 21 | printf(" "); 22 | printf(" "); 23 | for (j = 0; j < 16; j++) 24 | if (i + j < buflen) 25 | printf("%c", isprint(buf[i + j]) ? buf[i + j] : '.'); 26 | printf("\n"); 27 | } 28 | } 29 | 30 | static inline void pmlxzj_skip_lpe_data(FILE* file) { 31 | uint32_t len; 32 | fread(&len, sizeof(len), 1, file); 33 | fseek(file, (long)len, SEEK_CUR); 34 | } 35 | 36 | static inline void pmlxzj_util_copy(FILE* f_dst, FILE* f_src, size_t len) { 37 | char* buffer = malloc(4096); 38 | size_t bytes_read; 39 | while (len > 0) { 40 | bytes_read = fread(buffer, 1, PMLXZJ_MIN(4096, len), f_src); 41 | if (bytes_read == 0) { 42 | break; 43 | } 44 | len -= bytes_read; 45 | fwrite(buffer, 1, bytes_read, f_dst); 46 | } 47 | assert(len == 0); 48 | free(buffer); 49 | buffer = NULL; 50 | } 51 | 52 | static inline void pmlxzj_util_copy_file(FILE* f_dst, FILE* f_src) { 53 | fseek(f_src, 0, SEEK_END); 54 | size_t file_size = (size_t)ftell(f_src); 55 | 56 | fseek(f_src, 0, SEEK_SET); 57 | pmlxzj_util_copy(f_dst, f_src, file_size); 58 | } 59 | -------------------------------------------------------------------------------- /pmlxzj_win32.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "pmlxzj_utils.h" 4 | 5 | #ifdef _WIN32 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | static inline char* Win32W2U8(HANDLE hHeap, const wchar_t* text) { 12 | int size_needed = WideCharToMultiByte(CP_UTF8, 0, text, -1, NULL, 0, NULL, NULL); 13 | char* result = HeapAlloc(hHeap, HEAP_ZERO_MEMORY, size_needed * sizeof(char)); 14 | if (result != NULL) { 15 | WideCharToMultiByte(CP_UTF8, 0, text, -1, result, size_needed, NULL, NULL); 16 | } 17 | return result; 18 | } 19 | 20 | static inline void FixWindowsUnicodeSupport(char*** argv) { 21 | PMLXZJ_UNUSED_PARAMETER(argv); 22 | 23 | SetConsoleOutputCP(CP_UTF8); 24 | setlocale(LC_ALL, ".UTF8"); 25 | 26 | // Cast argv to UTF-8, if on UniversalCRT 27 | #ifdef _UCRT 28 | int argc = 0; 29 | wchar_t** argv_unicode = CommandLineToArgvW(GetCommandLineW(), &argc); 30 | 31 | char** p_argv = calloc(argc, sizeof(char*)); 32 | HANDLE hHeap = GetProcessHeap(); 33 | for (int i = 0; i < argc; i++) { 34 | p_argv[i] = Win32W2U8(hHeap, argv_unicode[i]); 35 | } 36 | *argv = p_argv; 37 | #endif 38 | } 39 | #else 40 | #define FixWindowsUnicodeSupport(...) \ 41 | do { \ 42 | } while (0) 43 | #endif 44 | --------------------------------------------------------------------------------