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 1e6f1e52..6f0e3e36 100644 Binary files a/packages/napcat-shell-loader/NapCatWinBootHook.dll and b/packages/napcat-shell-loader/NapCatWinBootHook.dll differ