From ec6aadaeea38ed3bca67b82bab3d7a9c0f91b8d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=8B=E7=93=9C=E4=B8=80=E5=8D=81=E9=9B=AA?= Date: Wed, 7 Oct 2026 00:07:42 +0800 Subject: [PATCH] fix(shell): support QQ 9.9.36 Windows file query aliases --- .github/tests/windows-loader/CMakeLists.txt | 3 + .github/tests/windows-loader/probe.cpp | 96 ++++++++++++++++++ .github/workflows/build-ci.yml | 20 ++++ .github/workflows/release-publish.yml | 14 +++ .../napcat-shell-loader/NapCatWinBootHook.dll | Bin 21504 -> 21504 bytes 5 files changed, 133 insertions(+) create mode 100644 .github/tests/windows-loader/CMakeLists.txt create mode 100644 .github/tests/windows-loader/probe.cpp diff --git a/.github/tests/windows-loader/CMakeLists.txt b/.github/tests/windows-loader/CMakeLists.txt new file mode 100644 index 00000000..29e19cf5 --- /dev/null +++ b/.github/tests/windows-loader/CMakeLists.txt @@ -0,0 +1,3 @@ +cmake_minimum_required(VERSION 3.20) +project(NapCatWindowsLoaderTest LANGUAGES CXX) +add_executable(windows-loader-test probe.cpp) diff --git a/.github/tests/windows-loader/probe.cpp b/.github/tests/windows-loader/probe.cpp new file mode 100644 index 00000000..53b8c736 --- /dev/null +++ b/.github/tests/windows-loader/probe.cpp @@ -0,0 +1,96 @@ +#include +#include +#include +#include +#include + +using QueryByName = BOOL(WINAPI *)(PCWSTR, FILE_INFO_BY_NAME_CLASS, PVOID, ULONG); + +int wmain(int argumentCount, wchar_t **arguments) +{ + if (argumentCount != 3) + return 2; + + const wchar_t *providers[] = {L"kernel32.dll", L"kernelbase.dll", L"api-ms-win-core-file-l2-1-4.dll"}; + HMODULE providerModules[3]{}; + QueryByName nativeQueries[3]{}; + unsigned char nativeCode[3][12]{}; + unsigned char nativeInfo[3][4][1024]{}; + BOOL nativeResults[3][4]{}; + DWORD nativeErrors[3][4]{}; + for (int providerIndex = 0; providerIndex < 3; ++providerIndex) + { + providerModules[providerIndex] = LoadLibraryW(providers[providerIndex]); + auto query = reinterpret_cast(GetProcAddress(providerModules[providerIndex], "GetFileInformationByName")); + if (!query) + return 3; + nativeQueries[providerIndex] = query; + std::memcpy(nativeCode[providerIndex], reinterpret_cast(query), sizeof(nativeCode[providerIndex])); + for (int infoClass = 0; infoClass < 4; ++infoClass) + { + SetLastError(ERROR_SUCCESS); + nativeResults[providerIndex][infoClass] = query(arguments[2], static_cast(infoClass), nativeInfo[providerIndex][infoClass], sizeof(nativeInfo[providerIndex][infoClass])); + nativeErrors[providerIndex][infoClass] = GetLastError(); + } + } + + FILE_STAT_BASIC_INFORMATION expected{}; + if (!nativeQueries[0](arguments[2], FileStatBasicByNameInfo, &expected, sizeof(expected))) + return 4; + if (_wputenv_s(L"NAPCAT_LOAD_PATH", arguments[2]) || _wputenv_s(L"NAPCAT_PATCH_PACKAGE", arguments[2])) + return 5; + if (!LoadLibraryW(arguments[1])) + { + std::printf("LoadLibrary failed: %lu\n", GetLastError()); + return 6; + } + + const std::wstring missingRoot = L"C:\\napcat-loader-test-" + std::to_wstring(GetCurrentProcessId()); + const std::wstring loaderPath = missingRoot + L"\\loadNapCat.js"; + const std::wstring packagePath = missingRoot + L"\\resources\\app\\package.json"; + const std::wstring unrelatedPath = missingRoot + L"\\unrelated.txt"; + const wchar_t *redirectedPaths[] = {loaderPath.c_str(), packagePath.c_str()}; + int failures = 0; + for (int providerIndex = 0; providerIndex < 3; ++providerIndex) + { + auto query = reinterpret_cast(GetProcAddress(providerModules[providerIndex], "GetFileInformationByName")); + bool preserved = std::memcmp(nativeCode[providerIndex], reinterpret_cast(nativeQueries[providerIndex]), sizeof(nativeCode[providerIndex])) == 0; + std::wprintf(L"%ls native code preserved: %ls\n", providers[providerIndex], preserved ? L"PASS" : L"FAIL"); + failures += !preserved; + for (int infoClass = 0; infoClass < 4; ++infoClass) + { + unsigned char actual[1024]{}; + SetLastError(ERROR_SUCCESS); + BOOL result = query(arguments[2], static_cast(infoClass), actual, sizeof(actual)); + DWORD error = GetLastError(); + bool passed = result == nativeResults[providerIndex][infoClass] && + (result ? std::memcmp(actual, nativeInfo[providerIndex][infoClass], sizeof(actual)) == 0 : error == nativeErrors[providerIndex][infoClass]); + std::wprintf(L"%ls information class %d native parity: %ls\n", providers[providerIndex], infoClass, passed ? L"PASS" : L"FAIL"); + failures += !passed; + } + for (const auto *redirectedPath : redirectedPaths) + { + FILE_STAT_BASIC_INFORMATION actual{}; + BOOL result = query(redirectedPath, FileStatBasicByNameInfo, &actual, sizeof(actual)); + bool passed = result && actual.EndOfFile.QuadPart == expected.EndOfFile.QuadPart && + actual.FileId.QuadPart == expected.FileId.QuadPart && actual.FileAttributes == expected.FileAttributes; + std::wprintf(L"%ls redirect %ls: %ls\n", providers[providerIndex], redirectedPath, passed ? L"PASS" : L"FAIL"); + failures += !passed; + } + FILE_STAT_BASIC_INFORMATION missing{}; + SetLastError(ERROR_SUCCESS); + BOOL nativeResult = nativeQueries[providerIndex](unrelatedPath.c_str(), FileStatBasicByNameInfo, &missing, sizeof(missing)); + DWORD nativeError = GetLastError(); + SetLastError(ERROR_SUCCESS); + BOOL result = query(unrelatedPath.c_str(), FileStatBasicByNameInfo, &missing, sizeof(missing)); + bool passed = !result && result == nativeResult && GetLastError() == nativeError; + std::wprintf(L"%ls unrelated missing file: %ls\n", providers[providerIndex], passed ? L"PASS" : L"FAIL"); + failures += !passed; + } + FILE_STAT_BASIC_INFORMATION direct{}; + BOOL result = GetFileInformationByName(loaderPath.c_str(), FileStatBasicByNameInfo, &direct, sizeof(direct)); + bool passed = result && direct.FileId.QuadPart == expected.FileId.QuadPart && direct.EndOfFile.QuadPart == expected.EndOfFile.QuadPart; + std::printf("static import redirection: %s\n", passed ? "PASS" : "FAIL"); + failures += !passed; + return failures ? 1 : 0; +} diff --git a/.github/workflows/build-ci.yml b/.github/workflows/build-ci.yml index 89a13324..fca746bd 100644 --- a/.github/workflows/build-ci.yml +++ b/.github/workflows/build-ci.yml @@ -9,6 +9,26 @@ on: permissions: write-all jobs: + windows-loader-test: + name: Windows Loader Test + needs: build + runs-on: windows-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/download-artifact@v7 + with: + name: NapCat.Shell + path: shell-dist + - name: Verify Windows loader file queries + shell: pwsh + run: | + cmake -S .github/tests/windows-loader -B "$env:RUNNER_TEMP/windows-loader-test" -A x64 + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + cmake --build "$env:RUNNER_TEMP/windows-loader-test" --config Release + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & "$env:RUNNER_TEMP/windows-loader-test/Release/windows-loader-test.exe" "$pwd/shell-dist/NapCatWinBootHook.dll" "$pwd/shell-dist/loadNapCat.js" + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + typecheck-and-test: name: Type Check & Test runs-on: ubuntu-latest diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index cbb72dd7..76225867 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -47,6 +47,7 @@ jobs: - name: Build NapCat.${{ matrix.target.name }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NAPCAT_VERSION: ${{ inputs.version }} run: | pnpm i pnpm --filter napcat-webui-frontend run build || exit 1 @@ -192,6 +193,9 @@ jobs: needs: assemble-windows-node runs-on: windows-latest steps: + - uses: actions/checkout@v5 + with: + ref: ${{ inputs.version }} - uses: actions/download-artifact@v7 with: name: NapCat.Shell.Windows.Node @@ -206,6 +210,16 @@ jobs: & .\node.exe -e "const wrapper = require('./wrapper.node'); if (!wrapper.NodeQQNTWrapperUtil) throw new Error('Missing NodeQQNTWrapperUtil'); console.log('wrapper.node native-load PASS'); process.exit(0);" if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + - name: Verify Windows loader file queries + shell: pwsh + run: | + cmake -S .github/tests/windows-loader -B "$env:RUNNER_TEMP/windows-loader-test" -A x64 + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + cmake --build "$env:RUNNER_TEMP/windows-loader-test" --config Release + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & "$env:RUNNER_TEMP/windows-loader-test/Release/windows-loader-test.exe" "$pwd/windows-node/napcat/NapCatWinBootHook.dll" "$pwd/windows-node/napcat/loadNapCat.js" + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + release-napcat: needs: [build, assemble-windows-node, validate-windows-node] runs-on: ubuntu-latest diff --git a/packages/napcat-shell-loader/NapCatWinBootHook.dll b/packages/napcat-shell-loader/NapCatWinBootHook.dll index 1e6f1e525e08b630aa5165b1393c13d659944a2e..6f0e3e36d36ebac1e6fb1f4ac2dedf440092cd11 100644 GIT binary patch delta 6067 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