diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b59514..dba9d89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,24 @@ All notable project changes are documented here. The project follows semantic versioning after the first generally available release. +## [0.4.0-rc4] - 2026-08-20 + +### Added + +- Original blue shield/cleanup application icon embedded at 16–256 pixel sizes for Explorer, the title bar, taskbar, and UAC display. +- The transparent high-resolution icon source and multi-resolution ICO are included in the open-source package. + +### Fixed + +- RU/EN switching now redraws the dialog atomically, preventing labels, buttons, and some list headers from appearing blank until a later repaint. +- The scan-complete status and count summary at the bottom are immediately regenerated in the selected language. +- Product, profile, and certificate selections are preserved while translated lists are rebuilt. + +### Validation + +- Automated GUI probing confirmed that all affected captions, the status, and the product/license/certificate summary change from Russian to English immediately. +- Both 16- and 32-pixel icon resources load successfully from the release EXE; core tests and the Release x86 build pass. + ## [0.4.0-rc3] - 2026-08-20 ### Changed diff --git a/CryptoProCleanup.vcxproj b/CryptoProCleanup.vcxproj index feba097..1f97d9b 100644 --- a/CryptoProCleanup.vcxproj +++ b/CryptoProCleanup.vcxproj @@ -38,5 +38,7 @@ + + diff --git a/README.en.md b/README.en.md index ebfc246..ca032f5 100644 --- a/README.en.md +++ b/README.en.md @@ -5,7 +5,7 @@ [![MIT License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) [![Windows 7–11](https://img.shields.io/badge/Windows-7%20SP1%E2%80%9311-0078D6.svg)](#system-requirements) -> **Status: 0.4.0 RC3.** A user successfully completed full removal on a live Windows 10 x64 system. Detection of products and the complete license plus public-certificate export are now confirmed on a real connected Windows 7 x86 disk. RC3 adds responsive background scanning with live indication. +> **Status: 0.4.0 RC4.** A user successfully completed full removal on a live Windows 10 x64 system. Detection of products and the complete license plus public-certificate export are confirmed on a real connected Windows 7 x86 disk. RC4 adds a recognizable application icon and fixes complete repaint plus bottom-summary translation when switching RU/EN. An unofficial portable utility for backing up license identifiers and public certificates, controlled removal of installed CryptoPro products, and rescue from a disconnected Windows 7 SP1 through Windows 11 installation. @@ -22,6 +22,7 @@ The utility deliberately preserves Windows certificate stores, hardware tokens, ## Features - native portable Unicode Win32 GUI with RU/EN languages and no .NET dependency; +- original multi-resolution application icon for Explorer, taskbar, window chrome, and UAC; - resizable/maximizable native window with adaptive tables, system-DPI scaling, and Windows visual styles; - verified 32/64-bit product discovery by publisher and MSI/EXE metadata; - complete license display, clipboard copy, and confidential backup; diff --git a/README.md b/README.md index 8d1cc2b..3a207bb 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![MIT License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) [![Windows 7–11](https://img.shields.io/badge/Windows-7%20SP1%E2%80%9311-0078D6.svg)](#системные-требования) -> **Статус: релиз-кандидат 0.4.0 RC3.** Полное удаление успешно проверено пользователем на живой Windows 10 x64. На реальном подключённом диске Windows 7 x86 подтверждены обнаружение продуктов, полный номер лицензии и экспорт открытых сертификатов. RC3 добавляет отзывчивое фоновое сканирование с живой индикацией. +> **Статус: релиз-кандидат 0.4.0 RC4.** Полное удаление успешно проверено пользователем на живой Windows 10 x64. На реальном подключённом диске Windows 7 x86 подтверждены обнаружение продуктов, полный номер лицензии и экспорт открытых сертификатов. RC4 добавляет узнаваемую иконку и исправляет полную перерисовку и нижний итог при переключении RU/EN. Неофициальная portable-утилита для резервного копирования лицензий и открытых сертификатов, контролируемого удаления установленных продуктов CryptoPro и спасения данных из отключённой Windows 7 SP1–Windows 11. @@ -27,6 +27,7 @@ ## Возможности - один нативный x86 EXE без .NET/PowerShell и внешнего runtime; +- оригинальная многоразмерная иконка приложения для Проводника, панели задач, окна и UAC; - изменяемое и максимизируемое окно с адаптивными таблицами, системным DPI-масштабированием и нативным современным оформлением; - поиск продуктов по подтверждённому издателю в 32- и 64-битных ветках реестра; - RU/EN интерфейс, выбор продуктов и локальных профилей; diff --git a/assets/CryptoProCleanup.ico b/assets/CryptoProCleanup.ico new file mode 100644 index 0000000..86f0fec Binary files /dev/null and b/assets/CryptoProCleanup.ico differ diff --git a/assets/app-icon-source.png b/assets/app-icon-source.png new file mode 100644 index 0000000..eab329c Binary files /dev/null and b/assets/app-icon-source.png differ diff --git a/docs/RELEASE_NOTES_0.4.0-rc4.md b/docs/RELEASE_NOTES_0.4.0-rc4.md new file mode 100644 index 0000000..b813074 --- /dev/null +++ b/docs/RELEASE_NOTES_0.4.0-rc4.md @@ -0,0 +1,18 @@ +# КриптоПро Очистка 0.4.0 RC4 + +В этом релизе у программы появилась собственная узнаваемая иконка, а переключение русского и английского интерфейса стало полностью синхронным. + +## Иконка + +- оригинальный знак «защитный щит + безопасная очистка» не копирует фирменную символику CryptoPro; +- в EXE встроены размеры от 16×16 до 256×256 для Проводника, панели задач, заголовка окна и UAC; +- прозрачный исходник PNG и многоразмерный ICO входят в архив исходников. + +## Исправление RU/EN + +- все элементы окна теперь обновляются одной атомарной перерисовкой; +- подписи, кнопки и заголовки первых столбцов больше не остаются временно пустыми; +- нижний статус `Ready. No changes were made.` и строка `Products found…` сразу переводятся на выбранный язык; +- отмеченные продукты, профили и сертификаты сохраняют свои флажки при перестроении таблиц. + +Алгоритмы сканирования, удаления, экспорта сертификатов и защиты закрытых ключей не изменялись. diff --git a/docs/TEST_MATRIX.md b/docs/TEST_MATRIX.md index 7552654..6717d63 100644 --- a/docs/TEST_MATRIX.md +++ b/docs/TEST_MATRIX.md @@ -42,6 +42,7 @@ For each VM retain the initial JSON report, final JSON report, cleanup log, prod - 2026-08-20: user testing of 0.4.0 RC2 on the real connected Windows 7 x86 disk confirmed product detection, the correct complete license identifier, and successful public-certificate export. RC3 moves this scan to a responsive background worker with continuous progress indication. - 2026-08-20: read-only synthetic disconnected-Windows fixture passed with 2 confirmed CryptoPro products, 4 license candidates, 1 inactive profile, and 7 public certificates from `AppData\Roaming\Microsoft\SystemCertificates\My\Certificates`; both `SOFTWARE` and `SYSTEM` temporary mounts unloaded successfully. - 2026-08-20: RC2 implementation passed unit tests, the non-destructive live integration scan, and MSVC C++ static analysis with no warnings. No `CryptoProCleanup_Offline_*` registry mounts remained after testing; sensitive hive/certificate fixture copies were removed. +- 2026-08-20: RC4 automated GUI probing confirmed immediate RU-to-EN translation of affected labels/buttons, status, and the product/license/certificate summary. Embedded 16- and 32-pixel icon resources loaded successfully from the Release EXE. - The running Windows directory is rejected as an offline target. A destructive disconnected-Windows VM test is still pending. - 2026-08-19: MSVC C++ static analysis passed with no warnings. Offline scanning also compares the actual `SOFTWARE` and `SYSTEM` hive file identities so a filesystem alias to the running Windows installation is rejected. - Destructive scenarios and the remaining OS/CSP matrix, including CSP 4.x, are still mandatory before marking a build generally available. diff --git a/scripts/package.ps1 b/scripts/package.ps1 index 85a9cbd..98c8a05 100644 --- a/scripts/package.ps1 +++ b/scripts/package.ps1 @@ -22,7 +22,7 @@ Copy-Item -LiteralPath (Join-Path $projectRoot 'docs') -Destination $packageRoot $sourceStage = Join-Path $packageRoot 'source' New-Item -ItemType Directory -Path $sourceStage -Force | Out-Null -foreach ($directory in @('.github', 'src', 'tests', 'scripts', 'docs')) { Copy-Item -LiteralPath (Join-Path $projectRoot $directory) -Destination $sourceStage -Recurse } +foreach ($directory in @('.github', 'assets', 'src', 'tests', 'scripts', 'docs')) { Copy-Item -LiteralPath (Join-Path $projectRoot $directory) -Destination $sourceStage -Recurse } foreach ($file in @('.gitattributes', '.gitignore', 'CryptoProCleanup.sln', 'CryptoProCleanup.vcxproj', 'CryptoProCleanupTests.vcxproj', 'README.md', 'README.en.md', 'LICENSE', 'CHANGELOG.md', 'CONTRIBUTING.md', 'SECURITY.md')) { Copy-Item -LiteralPath (Join-Path $projectRoot $file) -Destination $sourceStage } $sourceZip = Join-Path $packageRoot "CryptoProCleanup-$Version-source.zip" diff --git a/src/cleanup.hpp b/src/cleanup.hpp index 2e2bddf..178702b 100644 --- a/src/cleanup.hpp +++ b/src/cleanup.hpp @@ -10,7 +10,7 @@ namespace cpc { -constexpr wchar_t kVersion[] = L"0.4.0-rc3"; +constexpr wchar_t kVersion[] = L"0.4.0-rc4"; enum class Language { Russian, English }; enum class RiskLevel { Normal, High }; diff --git a/src/gui.cpp b/src/gui.cpp index e19953c..31d735f 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -28,6 +28,12 @@ struct OfflineProgressMessage { int percent = 0; }; +enum class SummaryKind { + None, + LiveScan, + OfflineScan +}; + struct AppState { HWND window = nullptr; Language language = Language::English; @@ -49,6 +55,8 @@ struct AppState { bool busy = false; bool offlineScanRunning = false; std::thread offlineScanThread; + SummaryKind summaryKind = SummaryKind::None; + Language summaryLanguage = Language::English; }; struct ConfirmState { @@ -244,6 +252,56 @@ void AddLogLine(AppState& state, const std::wstring& line) { if (!state.logPath.empty()) AppendLog(state.logPath, line); } +std::wstring LiveScanSummary(const AppState& state) { + std::wostringstream summary; + summary << Tr(state.language, L"Найдено продуктов: ", L"Products found: ") << state.scan.products.size() + << Tr(state.language, L", лицензий: ", L", licenses: ") << state.scan.licenses.size() + << Tr(state.language, L", открытых сертификатов: ", L", public certificates: ") + << state.scan.certificates.size(); + return summary.str(); +} + +std::wstring OfflineScanSummary(const AppState& state) { + if (!state.offline.valid) + return Tr(state.language, L"Офлайн-система не распознана.", + L"Offline system was not recognized."); + std::wostringstream summary; + summary << Tr(state.language, L"Офлайн: продуктов ", L"Offline: products ") << state.offline.scan.products.size() + << Tr(state.language, L", лицензий ", L", licenses ") << state.offline.scan.licenses.size() + << Tr(state.language, L", профилей ", L", profiles ") << state.offline.scan.profiles.size() + << Tr(state.language, L", открытых сертификатов ", L", public certificates ") << state.offline.scan.certificates.size() + << Tr(state.language, L", подтверждённых целей ", L", verified targets ") << state.offline.targets.size() + << (state.offline.cleanupCapable ? Tr(state.language, L", очистка доступна", L", cleanup available") + : Tr(state.language, L", только спасение данных", L", rescue only")); + return summary.str(); +} + +void RefreshLocalizedSummary(AppState& state) { + if (state.summaryKind == SummaryKind::None || !state.logPath.empty()) return; + const bool live = state.summaryKind == SummaryKind::LiveScan; + const std::wstring summary = live ? LiveScanSummary(state) : OfflineScanSummary(state); + const auto& warnings = live ? state.scan.warnings : state.offline.scan.warnings; + std::wstring logText = summary; + if (!warnings.empty() && state.summaryLanguage != state.language) { + logText += L"\r\n" + Tr(state.language, + L"Примечание: предупреждения ниже сформированы до переключения языка. Для их перевода повторите сканирование.", + L"Note: the warnings below were generated before the language switch. Scan again to translate them."); + } + for (const auto& warning : warnings) logText += L"\r\n" + warning; + SetWindowTextW(GetDlgItem(state.window, IDC_LOG), logText.c_str()); + if (live) { + SetText(state.window, IDC_STATUS, + Tr(state.language, L"Готово. Изменения не выполнялись.", + L"Ready. No changes were made.")); + } else if (state.offline.valid) { + SetText(state.window, IDC_STATUS, + Tr(state.language, L"Офлайн-сканирование завершено. Изменения не выполнялись.", + L"Offline scan completed. No changes were made.")); + } else { + SetText(state.window, IDC_STATUS, summary); + } +} + std::wstring DefaultBackupFolder() { PWSTR raw = nullptr; std::wstring folder; @@ -483,7 +541,11 @@ void UpdateTabVisibility(AppState& state) { for (const int id : offlineControls) ShowWindow(GetDlgItem(state.window, id), selected == 2 ? SW_SHOW : SW_HIDE); } +void ReadSelections(AppState& state); +void ReadOfflineSelections(AppState& state); + void ApplyLanguage(AppState& state) { + SendMessageW(state.window, WM_SETREDRAW, FALSE, 0); const bool ru = state.language == Language::Russian; const std::wstring applicationName = ru ? L"КриптоПро Очистка" : L"CryptoPro Cleanup Utility"; const std::wstring versionedName = applicationName + L" " + kVersion; @@ -544,6 +606,10 @@ void ApplyLanguage(AppState& state) { if (!state.scan.products.empty() || !state.scan.profiles.empty() || !state.scan.certificates.empty()) PopulateLists(state); if (state.offline.valid) PopulateOfflineLists(state); UpdateTabVisibility(state); + RefreshLocalizedSummary(state); + SendMessageW(state.window, WM_SETREDRAW, TRUE, 0); + RedrawWindow(state.window, nullptr, nullptr, + RDW_INVALIDATE | RDW_ERASE | RDW_ALLCHILDREN | RDW_UPDATENOW); } void SetBusy(AppState& state, bool busy) { @@ -810,23 +876,15 @@ void ReadOfflineSelections(AppState& state) { void FinishOfflineScan(AppState& state, OfflineScanResult&& result) { state.offline = std::move(result); PopulateOfflineLists(state); - std::wostringstream summary; if (state.offline.valid) { SetText(state.window, IDC_OFFLINE_PATH, state.offline.windowsDirectory); - summary << Tr(state.language, L"Офлайн: продуктов ", L"Offline: products ") << state.offline.scan.products.size() - << Tr(state.language, L", лицензий ", L", licenses ") << state.offline.scan.licenses.size() - << Tr(state.language, L", профилей ", L", profiles ") << state.offline.scan.profiles.size() - << Tr(state.language, L", открытых сертификатов ", L", public certificates ") << state.offline.scan.certificates.size() - << Tr(state.language, L", подтверждённых целей ", L", verified targets ") << state.offline.targets.size() - << (state.offline.cleanupCapable ? Tr(state.language, L", очистка доступна", L", cleanup available") - : Tr(state.language, L", только спасение данных", L", rescue only")); - UpdateProgress(state, Tr(state.language, L"Офлайн-сканирование завершено. Изменения не выполнялись.", L"Offline scan completed. No changes were made."), 100); - } else { - summary << Tr(state.language, L"Офлайн-система не распознана.", L"Offline system was not recognized."); - UpdateProgress(state, summary.str(), 100); } - AddLogLine(state, summary.str()); - for (const auto& warning : state.offline.scan.warnings) AddLogLine(state, warning); + state.summaryKind = SummaryKind::OfflineScan; + state.summaryLanguage = state.language; + SendDlgItemMessageW(state.window, IDC_PROGRESS, PBM_SETPOS, 100, 0); + RefreshLocalizedSummary(state); + RedrawWindow(state.window, nullptr, nullptr, + RDW_INVALIDATE | RDW_ALLCHILDREN | RDW_UPDATENOW); SetBusy(state, false); if (!state.offline.valid || (state.offline.scan.products.empty() && state.offline.scan.certificates.empty())) ShowOfflineDiagnostics(state); @@ -970,13 +1028,12 @@ void DoScan(AppState& state) { SetWindowTextW(GetDlgItem(state.window, IDC_LOG), L""); state.scan = ScanSystem(state.language, [&](const std::wstring& message, int percent) { UpdateProgress(state, message, percent); }); PopulateLists(state); - std::wostringstream summary; - summary << Tr(state.language, L"Найдено продуктов: ", L"Products found: ") << state.scan.products.size() - << Tr(state.language, L", лицензий: ", L", licenses: ") << state.scan.licenses.size() - << Tr(state.language, L", открытых сертификатов: ", L", public certificates: ") << state.scan.certificates.size(); - AddLogLine(state, summary.str()); - for (const auto& warning : state.scan.warnings) AddLogLine(state, warning); - UpdateProgress(state, Tr(state.language, L"Готово. Изменения не выполнялись.", L"Ready. No changes were made."), 100); + state.summaryKind = SummaryKind::LiveScan; + state.summaryLanguage = state.language; + SendDlgItemMessageW(state.window, IDC_PROGRESS, PBM_SETPOS, 100, 0); + RefreshLocalizedSummary(state); + RedrawWindow(state.window, nullptr, nullptr, + RDW_INVALIDATE | RDW_ALLCHILDREN | RDW_UPDATENOW); SetBusy(state, false); } @@ -1120,6 +1177,15 @@ INT_PTR CALLBACK MainDialogProc(HWND dialog, UINT message, WPARAM wParam, LPARAM state = reinterpret_cast(lParam); state->window = dialog; SetWindowLongPtrW(dialog, DWLP_USER, reinterpret_cast(state)); + HINSTANCE module = GetModuleHandleW(nullptr); + HICON largeIcon = reinterpret_cast(LoadImageW( + module, MAKEINTRESOURCEW(IDI_APP_ICON), IMAGE_ICON, + GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON), LR_DEFAULTCOLOR | LR_SHARED)); + HICON smallIcon = reinterpret_cast(LoadImageW( + module, MAKEINTRESOURCEW(IDI_APP_ICON), IMAGE_ICON, + GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON), LR_DEFAULTCOLOR | LR_SHARED)); + if (largeIcon) SendMessageW(dialog, WM_SETICON, ICON_BIG, reinterpret_cast(largeIcon)); + if (smallIcon) SendMessageW(dialog, WM_SETICON, ICON_SMALL, reinterpret_cast(smallIcon)); SendDlgItemMessageW(dialog, IDC_PROGRESS, PBM_SETRANGE, 0, MAKELPARAM(0, 100)); HWND language = GetDlgItem(dialog, IDC_LANGUAGE); ComboBox_AddString(language, L"Русский"); @@ -1198,6 +1264,8 @@ INT_PTR CALLBACK MainDialogProc(HWND dialog, UINT message, WPARAM wParam, LPARAM switch (LOWORD(wParam)) { case IDC_LANGUAGE: if (HIWORD(wParam) == CBN_SELCHANGE && !state->busy) { + ReadSelections(*state); + ReadOfflineSelections(*state); state->language = ComboBox_GetCurSel(GetDlgItem(dialog, IDC_LANGUAGE)) == 0 ? Language::Russian : Language::English; ApplyLanguage(*state); } diff --git a/src/resource.h b/src/resource.h index 8286997..c45ae90 100644 --- a/src/resource.h +++ b/src/resource.h @@ -4,6 +4,8 @@ #define IDD_TYPED_CONFIRM 102 #define IDD_LICENSES 103 +#define IDI_APP_ICON 201 + #define IDC_TITLE 1001 #define IDC_DISCLAIMER 1002 #define IDC_PRODUCTS_LABEL 1003 diff --git a/src/resources.rc b/src/resources.rc index 280acc7..b2d7d32 100644 --- a/src/resources.rc +++ b/src/resources.rc @@ -3,6 +3,8 @@ #include #include "resource.h" +IDI_APP_ICON ICON "../assets/CryptoProCleanup.ico" + IDD_MAIN DIALOGEX 0, 0, 760, 610 STYLE DS_SETFONT | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_THICKFRAME | WS_CLIPCHILDREN EXSTYLE WS_EX_APPWINDOW