C#:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Threading;
namespace RUST_PLUGIN_SCANNER {
public partial class MainWindow : Window {
private string _sevenZipExePath = "";
private static readonly HttpClient Client = new HttpClient {
Timeout = TimeSpan.FromSeconds(30)
}
;
// XenForo REST API Config
private const string XENFORO_BASE_URL = "https://rustcore.top";
private const string XENFORO_API_KEY = "vAZteAiJ8zFs80ZVdnrG56eGjBZvcmUz";
private const string XENFORO_USER_ID = "1";
// 15FPS
private const int FREE_PLUGINS_CATEGORY_ID = 2;
private const int FREE_PLUGINS_FORUM_NODE_ID = 4;
private const int FREE_PREFIX_ID = 1;
private const int MAX_DESCRIPTION_LENGTH = 48000;
private readonly HashSet<string> _processedVersions = new(StringComparer.OrdinalIgnoreCase);
// Индекс версий плагинов на сайте: CanonicalName -> Set of NormalizedVersions
private readonly Dictionary<string, HashSet<string>> _sitePluginVersions = new(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<string> _archiveExtensions = new(StringComparer.OrdinalIgnoreCase) {
".zip", ".rar", ".7z", ".tar", ".gz", ".xz", ".bz2", ".cab", ".iso"
}
;
// Сессионные пароли (запоминаются автоматически при успешном открытии)
private readonly List<string> _sessionPasswords = new();
// Активные токены отмены для открытых окон дешифровки
private readonly List<CancellationTokenSource> _activeUnlockCts = new();
private int _unlockWindowCascadeIndex = 0;
// Загруженный словарь паролей из passwords.json (Embedded Resource для единого EXE + внешний файл)
private static readonly List<string> EmbeddedPasswords = LoadEmbeddedPasswords();
private static List<string> LoadEmbeddedPasswords() {
var list = new List<string>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
void Add(string? p) {
if (string.IsNullOrWhiteSpace(p)) return;
p = p.Trim();
if (p.Length >= 1 && p.Length <= 64 && seen.Add(p)) {
list.Add(p);
}
}
// 1.Попытка загрузить внешний passwords.json рядом с приложением (если пользователь добавил свои)
try {
string localPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "passwords.json");
if (File.Exists(localPath)) {
string json = File.ReadAllText(localPath);
var parsed = JsonSerializer.Deserialize<List<string>>(json);
if (parsed != null) {
foreach (var p in parsed) Add(p);
}
}
}
catch {
}
// 2.Загрузка встроенного passwords.json из EmbeddedResource (запакованного прямо в один EXE)
try {
var asm = Assembly.GetExecutingAssembly();
using var stream = asm.GetManifestResourceStream("RUST_PLUGIN_SCANNER.passwords.json");
if (stream != null) {
var parsed = JsonSerializer.Deserialize<List<string>>(stream);
if (parsed != null) {
foreach (var p in parsed) Add(p);
}
}
}
catch {
}
// Фолбэк на случай повреждения ресурсов
if (list.Count == 0) {
for (int n = 1111; n <= 1250; n++) Add(n.ToString());
for (int n = 1000; n <= 1050; n++) Add(n.ToString());
string[] basic = {
"1234", "12345", "123456", "0000", "1111", "2222", "rust", "rustmods", "oxide", "carbon", "umod", "password"
}
;
foreach (var b in basic) Add(b);
}
return list;
}
private int _totalProcessed = 0;
private int _totalSavedLocal = 0;
private int _totalUploaded = 0;
private int _totalSkipped = 0;
private int _totalUnlockedArchives = 0;
private bool _isScanning = false;
private CancellationTokenSource? _scanCts;
private static readonly SemaphoreSlim _archiveUnlockSemaphore = new SemaphoreSlim(2, 2);
private string _cachedTargetDir = "";
private long _lastStatusUpdateTick = 0;
private long _lastCountersUpdateTick = 0;
public MainWindow() {
InitializeComponent();
LoadDrives();
_ = IndexXenForoResourcesAsync();
}
private void LoadDrives() {
CmbDrives.Items.Clear();
CmbDrives.Items.Add("0.Сканировать ВСЕ диски");
foreach (var drive in DriveInfo.GetDrives().Where(d => d.IsReady)) {
CmbDrives.Items.Add(drive.Name);
}
CmbDrives.SelectedIndex = 0;
}
// ================= XENFORO REST API ИНДЕКСАЦИЯ И ПРОВЕРКА ВЕРСИЙ =================
private static string NormalizeVersion(string rawVer) {
if (string.IsNullOrWhiteSpace(rawVer)) return "1.0.0";
// 1.Удаляем невидимые Unicode-символы ( Word Joiner, zero-width spaces, BOM, управляющие символы)
string clean = Regex.Replace(rawVer, @"[\p{C}\p{Z}-]+", "");
clean = clean.TrimStart('v', 'V').Trim();
// 2.Ищем корректную цифровую версию (например 3.0.0, 1.2.4)
var match = Regex.Match(clean, @"^(\d+(?:\.\d+)+)");
if (match.Success) {
return match.Groups[1].Value;
}
// 3.Если версия нестандартная, оставляем только безопасные символы имени файла
string safe = Regex.Replace(clean, @"[^a-zA-Z0-9\.\-_]", "");
return string.IsNullOrWhiteSpace(safe) ? "1.0.0" : safe;
}
private static string ToCanonicalString(string input) {
if (string.IsNullOrEmpty(input)) return "";
return Regex.Replace(input.ToLower(), @"[^a-z0-9]", "");
}
private void AddTitleToDuplicateIndex(string rawTitle, string rawVersion) {
if (string.IsNullOrWhiteSpace(rawTitle)) return;
string titleClean = rawTitle.Trim();
string baseName = titleClean;
string detectedVersion = rawVersion?.Trim() ?? "";
// Если версия указана прямо в конце заголовка (например "BetterChat 1.0.4", "SoFriends : 0.0.1" или "BetterChat v1.0.4")
var match = Regex.Match(titleClean, @"^(.*?)(?:\s*[:\-]\s*|\s+)v?(\d+(?:\.\d+)*[a-zA-Z0-9_\-\.]*)$", RegexOptions.IgnoreCase);
if (match.Success) {
baseName = match.Groups[1].Value.Trim();
if (string.IsNullOrEmpty(detectedVersion)) {
detectedVersion = match.Groups[2].Value.Trim();
}
}
string canonicalBase = ToCanonicalString(baseName);
string normVer = NormalizeVersion(detectedVersion);
lock (_sitePluginVersions) {
if (!_sitePluginVersions.ContainsKey(canonicalBase)) {
_sitePluginVersions[canonicalBase] = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
}
if (!string.IsNullOrEmpty(normVer)) {
_sitePluginVersions[canonicalBase].Add(normVer);
}
string fullCanonical = ToCanonicalString(titleClean);
if (!_sitePluginVersions.ContainsKey(fullCanonical)) {
_sitePluginVersions[fullCanonical] = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
}
if (!string.IsNullOrEmpty(normVer)) {
_sitePluginVersions[fullCanonical].Add(normVer);
}
}
}
private bool IsPluginSameVersionUploadedOnSite(string name, string version) {
string canonicalName = ToCanonicalString(name);
string normVer = NormalizeVersion(version);
lock (_sitePluginVersions) {
if (_sitePluginVersions.TryGetValue(canonicalName, out var versions)) {
// Если на сайте уже есть этот плагин с ТОЧНО ТАКОЙ ЖЕ версией — не загружаем
if (versions.Contains(normVer)) {
return true;
}
}
}
// Если плагина нет на сайте ИЛИ он есть, но С ДРУГОЙ ВЕРСИЕЙ — загружаем!
return false;
}
private async Task IndexXenForoResourcesAsync() {
try {
Log("[*] Индексация всех плагинов и версий на rustcore.top...", Colors.Cyan);
lock (_sitePluginVersions) {
_sitePluginVersions.Clear();
}
int count = 0;
// 1.Индексация ресурсов (категории 2-5)
for (int catId = 2; catId <= 5; catId++) {
int page = 1;
while (true) {
var req = new HttpRequestMessage(HttpMethod.Get, $"{XENFORO_BASE_URL}/api/resources/?category_id={catId}&page={page}");
req.Headers.Add("XF-Api-Key", XENFORO_API_KEY);
req.Headers.Add("XF-Api-User", XENFORO_USER_ID);
var resp = await Client.SendAsync(req);
if (!resp.IsSuccessStatusCode) break;
string json = await resp.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(json);
if (!doc.RootElement.TryGetProperty("resources", out var resArray) || resArray.GetArrayLength() == 0) break;
foreach (var res in resArray.EnumerateArray()) {
string title = res.GetProperty("title").GetString() ?? "";
string version = res.TryGetProperty("version", out var vProp) ? vProp.GetString() ?? "" : "";
AddTitleToDuplicateIndex(title, version);
count++;
}
if (!doc.RootElement.TryGetProperty("pagination", out var pageProp) || !pageProp.TryGetProperty("last_page", out var lastPageProp) || page >= lastPageProp.GetInt32()) break;
page++;
}
}
// 2.Индексация тем на форуме (Node ID 4)
int threadPage = 1;
while (true) {
var req = new HttpRequestMessage(HttpMethod.Get, $"{XENFORO_BASE_URL}/api/threads/?node_id={FREE_PLUGINS_FORUM_NODE_ID}&page={threadPage}");
req.Headers.Add("XF-Api-Key", XENFORO_API_KEY);
req.Headers.Add("XF-Api-User", XENFORO_USER_ID);
var resp = await Client.SendAsync(req);
if (!resp.IsSuccessStatusCode) break;
string json = await resp.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(json);
if (!doc.RootElement.TryGetProperty("threads", out var threadArray) || threadArray.GetArrayLength() == 0) break;
foreach (var t in threadArray.EnumerateArray()) {
string title = t.GetProperty("title").GetString() ?? "";
AddTitleToDuplicateIndex(title, "");
count++;
}
if (!doc.RootElement.TryGetProperty("pagination", out var pageProp) || !pageProp.TryGetProperty("last_page", out var lastPageProp) || threadPage >= lastPageProp.GetInt32()) break;
threadPage++;
}
Log($"✔ Проиндексировано {count} плагинов и тем на сайте.", Colors.SpringGreen);
}
catch (Exception ex) {
Log($"[!] Ошибка индексации XenForo: {ex.Message}", Colors.Yellow);
}
}
private async Task<bool> LivePreUploadServerCheckAsync(string name, string version) {
try {
string searchUrl = $"{XENFORO_BASE_URL}/api/resources/?category_id={FREE_PLUGINS_CATEGORY_ID}&title={Uri.EscapeDataString(name)}";
var req = new HttpRequestMessage(HttpMethod.Get, searchUrl);
req.Headers.Add("XF-Api-Key", XENFORO_API_KEY);
req.Headers.Add("XF-Api-User", XENFORO_USER_ID);
var resp = await Client.SendAsync(req);
if (resp.IsSuccessStatusCode) {
string json = await resp.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(json);
if (doc.RootElement.TryGetProperty("resources", out var resArray) && resArray.GetArrayLength() > 0) {
foreach (var res in resArray.EnumerateArray()) {
string title = res.GetProperty("title").GetString() ?? "";
string ver = res.TryGetProperty("version", out var vProp) ? vProp.GetString() ?? "" : "";
AddTitleToDuplicateIndex(title, ver);
}
// Проверяем, есть ли на сайте ресурс с ТОЙ ЖЕ САМОЙ версией
if (IsPluginSameVersionUploadedOnSite(name, version)) {
return true;
}
}
}
}
catch {
}
return false;
}
private async Task<bool> UploadPhysicalPluginToXenForoAsync(string localPath, string name, string version, string description, string content) {
if (await LivePreUploadServerCheckAsync(name, version)) {
Log($"[СЕРВЕР LIVE-CHECK] {name} v{version} с такой версией уже есть на сайте.Пропущен.", Colors.Gray);
_totalSkipped++;
UpdateCounters();
return false;
}
try {
// Заголовок в формате: "ИмяПлагина : Версия" (например "SoFriends : 0.0.1")
string title = $"{name} : {version}";
string fileName = Path.GetFileName(localPath);
// Шаг 1: Получение ключа вложения
var keyReq = new HttpRequestMessage(HttpMethod.Post, $"{XENFORO_BASE_URL}/api/attachments/new-key?api_bypass_permissions=1");
keyReq.Headers.Add("XF-Api-Key", XENFORO_API_KEY);
keyReq.Headers.Add("XF-Api-User", XENFORO_USER_ID);
var keyContent = new FormUrlEncodedContent(new[] {
new KeyValuePair<string, string>("type", "resource_version"), new KeyValuePair<string, string>("context[resource_category_id]", FREE_PLUGINS_CATEGORY_ID.ToString())
}
);
keyReq.Content = keyContent;
var keyResp = await Client.SendAsync(keyReq);
if (!keyResp.IsSuccessStatusCode) {
Log($"[!] Ошибка ключа вложения ({keyResp.StatusCode}): {await keyResp.Content.ReadAsStringAsync()}", Colors.OrangeRed);
return false;
}
string keyJson = await keyResp.Content.ReadAsStringAsync();
using var keyDoc = JsonDocument.Parse(keyJson);
string attachmentKey = keyDoc.RootElement.GetProperty("key").GetString() ?? "";
// Шаг 2: Загрузка физического файла.cs во вложение
var attachReq = new HttpRequestMessage(HttpMethod.Post, $"{XENFORO_BASE_URL}/api/attachments?api_bypass_permissions=1");
attachReq.Headers.Add("XF-Api-Key", XENFORO_API_KEY);
attachReq.Headers.Add("XF-Api-User", XENFORO_USER_ID);
using var form = new MultipartFormDataContent();
form.Add(new StringContent(attachmentKey), "key");
byte[] fileBytes = await File.ReadAllBytesAsync(localPath);
// Гарантируем чистое ASCII имя файла с расширением.cs для XenForo API
string safeAttachmentName = $"{CleanFileName(name)}_{NormalizeVersion(version)}.cs";
var fileContent = new ByteArrayContent(fileBytes);
fileContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("text/plain");
fileContent.Headers.ContentDisposition = new System.Net.Http.Headers.ContentDispositionHeaderValue("form-data") {
Name = "\"attachment\"", FileName = $"\"{safeAttachmentName}\""
}
;
form.Add(fileContent);
attachReq.Content = form;
var attachResp = await Client.SendAsync(attachReq);
if (!attachResp.IsSuccessStatusCode) {
Log($"[!] Ошибка передачи файла во вложение ({attachResp.StatusCode}): {await attachResp.Content.ReadAsStringAsync()}", Colors.OrangeRed);
return false;
}
string attachJson = await attachResp.Content.ReadAsStringAsync();
using var attachDoc = JsonDocument.Parse(attachJson);
int attachmentId = attachDoc.RootElement.GetProperty("attachment").GetProperty("attachment_id").GetInt32();
// Шаг 3: Создание физического downloadable ресурса
var resReq = new HttpRequestMessage(HttpMethod.Post, $"{XENFORO_BASE_URL}/api/resources/?api_bypass_permissions=1");
resReq.Headers.Add("XF-Api-Key", XENFORO_API_KEY);
resReq.Headers.Add("XF-Api-User", XENFORO_USER_ID);
string tagLine = description.Trim();
if (tagLine.Length > 95) tagLine = tagLine[..92] + "...";
if (string.IsNullOrWhiteSpace(tagLine)) tagLine = $"Rust Oxide plugin {name} v{version}";
string codeDisplay = content;
if (codeDisplay.Length > MAX_DESCRIPTION_LENGTH) codeDisplay = codeDisplay[..MAX_DESCRIPTION_LENGTH] + "\n//...[Code preview truncated due to length.Full source in attached file]...";
string bbcode = $"[CODE=csharp]\n{codeDisplay}\n
var resContent = new FormUrlEncodedContent(new[] {
new KeyValuePair<string, string>("title", title), new KeyValuePair<string, string>("resource_category_id", FREE_PLUGINS_CATEGORY_ID.ToString()), new KeyValuePair<string, string>("tag_line", tagLine), new KeyValuePair<string, string>("description", bbcode), new KeyValuePair<string, string>("resource_type", "download_local"), new KeyValuePair<string, string>("version_attachment_key", attachmentKey), new KeyValuePair<string, string>("version_string", version), new KeyValuePair<string, string>("prefix_id", FREE_PREFIX_ID.ToString()), new KeyValuePair<string, string>("tags", "free")
}
);
resReq.Content = resContent;
var resResp = await Client.SendAsync(resReq);
if (resResp.IsSuccessStatusCode) {
string resJson = await resResp.Content.ReadAsStringAsync();
using var resDoc = JsonDocument.Parse(resJson);
var resObj = resDoc.RootElement.GetProperty("resource");
string viewUrl = resObj.GetProperty("view_url").GetString() ?? "";
int rid = resObj.GetProperty("resource_id").GetInt32();
Log($"✔ [УСПЕХ ФОРУМ] Опубликован плагин: {title} -> ID {rid} | {viewUrl}", Colors.SpringGreen);
_totalUploaded++;
UpdateCounters();
AddTitleToDuplicateIndex(title, version);
return true;
}
else {
Log($"[!] Ошибка публикации ресурса ({resResp.StatusCode}): {await resResp.Content.ReadAsStringAsync()}", Colors.OrangeRed);
return false;
}
}
catch (Exception ex) {
Log($"[!] Исключение при загрузке на сайт: {ex.Message}", Colors.Red);
return false;
}
}
// ================= 7-ZIP УТИЛИТА =================
private async Task<bool> EnsureSevenZipInstalledAsync() {
string appDir = AppDomain.CurrentDomain.BaseDirectory;
string localExePath = Path.Combine(appDir, "7z.exe");
if (File.Exists(localExePath)) {
_sevenZipExePath = localExePath;
return true;
}
string programFiles64 = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "7-Zip", "7z.exe");
if (File.Exists(programFiles64)) {
_sevenZipExePath = programFiles64;
return true;
}
string programFilesx86 = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "7-Zip", "7z.exe");
if (File.Exists(programFilesx86)) {
_sevenZipExePath = programFilesx86;
return true;
}
string osPanelZip = @"C:\OSPanel\bin\7za.exe";
if (File.Exists(osPanelZip)) {
_sevenZipExePath = osPanelZip;
return true;
}
Log("⚠ 7-Zip не найден.Автоматическая загрузка консольных утилит...", Colors.Yellow);
UpdateStatus("Загрузка 7-Zip...");
return await ExtractOrDownloadFallbackExeAsync(appDir);
}
private async Task<bool> ExtractOrDownloadFallbackExeAsync(string targetDir) {
string exeUrl = "https://raw.githubusercontent.com/mcmilk/7-Zip-zstd/master/bin/x64/7z.exe";
string dllUrl = "https://raw.githubusercontent.com/mcmilk/7-Zip-zstd/master/bin/x64/7z.dll";
string localExe = Path.Combine(targetDir, "7z.exe");
string localDll = Path.Combine(targetDir, "7z.dll");
try {
if (!File.Exists(localExe)) {
byte[] exeBytes = await Client.GetByteArrayAsync(exeUrl);
await File.WriteAllBytesAsync(localExe, exeBytes);
}
if (!File.Exists(localDll)) {
byte[] dllBytes = await Client.GetByteArrayAsync(dllUrl);
await File.WriteAllBytesAsync(localDll, dllBytes);
}
_sevenZipExePath = localExe;
Log("✔ 7-Zip успешно загружен!", Colors.SpringGreen);
return true;
}
catch (Exception ex) {
Log($"[ОШИБКА 7-ZIP] Загрузка не удалась: {ex.Message}", Colors.OrangeRed);
return false;
}
}
// ================= АВТОМАТИЧЕСКАЯ ДЕШИФРОВКА АРХИВОВ И ФОНОВЫЕ ОКНА =================
private static string Escape7zArg(string arg) {
if (string.IsNullOrEmpty(arg)) return "";
return arg.Replace("\"", "\\\"");
}
private static void ExtractPasswordsFromText(string text, Action<string> addCand) {
if (string.IsNullOrWhiteSpace(text)) return;
// 1.Поиск шаблонов: пароль/password/pass/код/pin/key/unlock
var passMatches = Regex.Matches(text, @"(?i)(?:парол[ьяе][^\r\n:]*|password[^\r\n:]*|pass[^\r\n:]*|код[^\r\n:]*|code[^\r\n:]*|pin[^\r\n:]*|key[^\r\n:]*|ключ[^\r\n:]*)[:=\s\-]+([a-zA-Z0-9_\-\.@!#]+)");
foreach (Match m in passMatches) {
addCand(m.Groups[1].Value);
}
// 2.Ссылки на Telegram, VK, Discord
var linkMatches = Regex.Matches(text, @"(?i)(?:t\.me|vk\.com|discord\.gg)\/([a-zA-Z0-9_]+)");
foreach (Match lm in linkMatches) {
addCand(lm.Groups[1].Value);
addCand(lm.Value);
}
// 3.Доменные имена сайтов (например rustmods.ru, oxide-russia.ru, rustcore.top)
var domainMatches = Regex.Matches(text, @"([a-zA-Z0-9_-]+\.[a-zA-Z]{2,4})");
foreach (Match m in domainMatches) {
string dom = m.Groups[1].Value;
addCand(dom);
string nameOnly = dom.Split('.')[0];
addCand(nameOnly);
}
// 4.Содержимое в скобках [rustmods], (leak), {pass123}
var bracketMatches = Regex.Matches(text, @"[\[\(\{]([^\]\)\}]+)[\]\)\}]");
foreach (Match m in bracketMatches) {
string val = m.Groups[1].Value.Trim();
addCand(val);
var subParts = val.Split(new[] {
' ', '-', '_', '.'
}
, StringSplitOptions.RemoveEmptyEntries);
foreach (var p in subParts) addCand(p);
}
// 5.Сегменты и токены (слова/числа от 2 до 30 символов)
var tokens = text.Split(new[] {
' ', '_', '-', '.', '/', '\\'
}
, StringSplitOptions.RemoveEmptyEntries);
foreach (var token in tokens) {
if (token.Length >= 2 && token.Length <= 30) {
addCand(token);
}
}
}
private static string DecodeBytesSafe(byte[] bytes, int offset, int length) {
if (bytes == null || length <= 0 || offset + length > bytes.Length) return "";
try {
string utf8 = Encoding.UTF8.GetString(bytes, offset, length);
if (!utf8.Contains('�')) return utf8;
}
catch {
}
var sb = new StringBuilder(length);
for (int i = offset; i < offset + length; i++) {
byte b = bytes;
if (b < 128) {
sb.Append((char)b);
}
else if (b >= 0xC0 && b <= 0xFF) // Windows-1251: А-я
{
sb.Append((char)(0x0410 + (b - 0xC0)));
}
else if (b == 0xA8) sb.Append('Ё');
else if (b == 0xB8) sb.Append('ё');
else if (b >= 0x80 && b <= 0xAF) // CP866: А-п
{
sb.Append((char)(0x0410 + (b - 0x80)));
}
else if (b >= 0xE0 && b <= 0xEF) // CP866: р-я
{
sb.Append((char)(0x0440 + (b - 0xE0)));
}
else if (b == 0xF0) sb.Append('Ё');
else if (b == 0xF1) sb.Append('ё');
else {
sb.Append((char)b);
}
}
return sb.ToString();
}
private static void ExtractZipBinaryMetadataDeep(string zipPath, Action<string> addCand) {
try {
using var fs = new FileStream(zipPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
if (fs.Length < 22) return;
int searchLen = (int)Math.Min(fs.Length, 65557);
fs.Seek(-searchLen, SeekOrigin.End);
byte[] tailBuffer = new byte[searchLen];
fs.ReadExactly(tailBuffer, 0, searchLen);
int eocdIndex = -1;
for (int i = tailBuffer.Length - 22; i >= 0; i--) {
if (tailBuffer == 0x50 && tailBuffer[i + 1] == 0x4b && tailBuffer[i + 2] == 0x05 && tailBuffer[i + 3] == 0x06) {
eocdIndex = i;
break;
}
}
if (eocdIndex == -1) return;
// 1.Чтение глобального комментария ZIP-архива из EOCD
int commentLen = tailBuffer[eocdIndex + 20] | (tailBuffer[eocdIndex + 21] << 8);
if (commentLen > 0 && eocdIndex + 22 + commentLen <= tailBuffer.Length) {
string rawComment = DecodeBytesSafe(tailBuffer, eocdIndex + 22, commentLen);
ExtractPasswordsFromText(rawComment, addCand);
addCand(rawComment);
}
// 2.Смещение и разбор заголовков файлов в Central Directory (0x02014b50)
int cdEntries = tailBuffer[eocdIndex + 10] | (tailBuffer[eocdIndex + 11] << 8);
uint cdOffset = BitConverter.ToUInt32(tailBuffer, eocdIndex + 16);
if (cdOffset > 0 && cdOffset < fs.Length) {
fs.Seek(cdOffset, SeekOrigin.Begin);
byte[] header = new byte[46];
int readCount = Math.Min(cdEntries, 400);
for (int k = 0; k < readCount; k++) {
if (fs.Position + 46 > fs.Length) break;
int read = fs.Read(header, 0, 46);
if (read < 46) break;
if (header[0] != 0x50 || header[1] != 0x4b || header[2] != 0x01 || header[3] != 0x02) break;
int nameLen = header[28] | (header[29] << 8);
int extraLen = header[30] | (header[31] << 8);
int fileCommentLen = header[32] | (header[33] << 8);
if (nameLen > 0) {
byte[] nameBytes = new byte[nameLen];
if (fs.Read(nameBytes, 0, nameLen) == nameLen) {
string entryName = DecodeBytesSafe(nameBytes, 0, nameLen);
ExtractPasswordsFromText(entryName, addCand);
}
}
if (extraLen > 0) {
if (fs.Position + extraLen <= fs.Length) {
if (extraLen <= 1024) {
byte[] extraBytes = new byte[extraLen];
if (fs.Read(extraBytes, 0, extraLen) == extraLen) {
string extraStr = DecodeBytesSafe(extraBytes, 0, extraLen);
ExtractPasswordsFromText(extraStr, addCand);
}
}
else {
fs.Seek(extraLen, SeekOrigin.Current);
}
}
}
if (fileCommentLen > 0) {
if (fs.Position + fileCommentLen <= fs.Length) {
byte[] commentBytes = new byte[fileCommentLen];
if (fs.Read(commentBytes, 0, fileCommentLen) == fileCommentLen) {
string fileComment = DecodeBytesSafe(commentBytes, 0, fileCommentLen);
ExtractPasswordsFromText(fileComment, addCand);
addCand(fileComment);
}
}
}
}
}
}
catch {
}
}
private static void ScanArchiveRawBinaryStrings(string archivePath, Action<string> addCand) {
try {
var fi = new FileInfo(archivePath);
if (!fi.Exists || fi.Length == 0) return;
using var fs = new FileStream(archivePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
void ScanBuffer(byte[] buf, int count) {
if (count <= 4) return;
int start = -1;
for (int i = 0; i < count; i++) {
byte b = buf;
bool isPrintable = (b >= 32 && b <= 126) || b >= 128;
if (isPrintable) {
if (start == -1) start = i;
}
else {
if (start != -1) {
int len = i - start;
if (len >= 3 && len <= 120) {
string s = DecodeBytesSafe(buf, start, len);
ExtractPasswordsFromText(s, addCand);
}
start = -1;
}
}
}
if (start != -1) {
int len = count - start;
if (len >= 3 && len <= 120) {
string s = DecodeBytesSafe(buf, start, len);
ExtractPasswordsFromText(s, addCand);
}
}
}
// 1.Заголовок архива (первые 64 КБ)
int headSize = (int)Math.Min(fs.Length, 65536);
byte[] headBuffer = new byte[headSize];
int headRead = fs.Read(headBuffer, 0, headSize);
ScanBuffer(headBuffer, headRead);
// 2.Хвост архива (последние 131 КБ)
if (fs.Length > headSize) {
int tailSize = (int)Math.Min(fs.Length - headSize, 131072);
fs.Seek(-tailSize, SeekOrigin.End);
byte[] tailBuffer = new byte[tailSize];
int tailRead = fs.Read(tailBuffer, 0, tailSize);
ScanBuffer(tailBuffer, tailRead);
}
}
catch {
}
}
private async Task<List<string>> ExtractArchiveMetadataPasswordsAsync(string archivePath, CancellationToken ct) {
var list = new List<string>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
void AddCand(string? s) {
if (string.IsNullOrWhiteSpace(s)) return;
s = s.Trim().Trim('"', '\'', ':', ';', ',', '.', '[', ']', '(', ')', '{', '}');
if (s.Length >= 1 && s.Length <= 64 && seen.Add(s)) {
list.Add(s);
}
}
// 1.Глубокое бинарное чтение End of Central Directory и Central Directory для.zip
try {
if (archivePath.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) {
ExtractZipBinaryMetadataDeep(archivePath, AddCand);
}
}
catch {
}
// 2.Сканирование бинарных строк заголовка и хвоста любого архива (.zip,.rar,.7z)
try {
ScanArchiveRawBinaryStrings(archivePath, AddCand);
}
catch {
}
// 3.Метаданные и комментарии архива через 7-Zip (7z l -slt)
Process? listProc = null;
try {
listProc = new Process();
listProc.StartInfo.FileName = _sevenZipExePath;
listProc.StartInfo.Arguments = $"l -slt \"{archivePath}\" -p\"\" -bse0 -bsp0 -bd";
listProc.StartInfo.CreateNoWindow = true;
listProc.StartInfo.UseShellExecute = false;
listProc.StartInfo.RedirectStandardOutput = true;
listProc.StartInfo.RedirectStandardError = false;
listProc.Start();
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
linkedCts.CancelAfter(TimeSpan.FromSeconds(5));
string output = await listProc.StandardOutput.ReadToEndAsync(linkedCts.Token);
await listProc.WaitForExitAsync(linkedCts.Token);
var commentMatches = Regex.Matches(output, @"(?i)^Comment\s*=\s*(.+)$", RegexOptions.Multiline);
foreach (Match cm in commentMatches) {
ExtractPasswordsFromText(cm.Groups[1].Value, AddCand);
}
var pathMatches = Regex.Matches(output, @"(?i)^Path\s*=\s*(.+)$", RegexOptions.Multiline);
foreach (Match pm in pathMatches) {
ExtractPasswordsFromText(pm.Groups[1].Value, AddCand);
}
}
catch (OperationCanceledException) {
try {
listProc?.Kill();
}
catch {
}
}
catch {
try {
listProc?.Kill();
}
catch {
}
}
finally {
try {
listProc?.Dispose();
}
catch {
}
}
// 4.Извлечение незашифрованных текстовых файлов из архива (readme, info, pass, txt, url, diz, nfo)
try {
string tempDir = Path.Combine(Path.GetTempPath(), "RustMeta_" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempDir);
Process? metaExtractProc = null;
try {
metaExtractProc = new Process();
metaExtractProc.StartInfo.FileName = _sevenZipExePath;
metaExtractProc.StartInfo.Arguments = $"e \"{archivePath}\" -o\"{tempDir}\" \"*readme*\" \"*read_me*\" \"*read*\" \"*pass*\" \"*парол*\" \"*инфо*\" \"*info*\" \"*.txt\" \"*.nfo\" \"*.diz\" \"*.ion\" \"*.url\" \"*.me\" \"*.html\" \"*.htm\" \"*.json\" -p\"\" -aoa -y -bso0 -bse0 -bsp0 -bd";
metaExtractProc.StartInfo.CreateNoWindow = true;
metaExtractProc.StartInfo.UseShellExecute = false;
metaExtractProc.StartInfo.RedirectStandardOutput = false;
metaExtractProc.StartInfo.RedirectStandardError = false;
metaExtractProc.Start();
using var lCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
lCts.CancelAfter(TimeSpan.FromSeconds(4));
await metaExtractProc.WaitForExitAsync(lCts.Token);
if (Directory.Exists(tempDir)) {
foreach (var file in Directory.EnumerateFiles(tempDir, "*", SearchOption.AllDirectories)) {
try {
if (new FileInfo(file).Length < 2 * 1024 * 1024) {
string fileText = await File.ReadAllTextAsync(file, ct);
ExtractPasswordsFromText(fileText, AddCand);
}
}
catch {
}
}
}
}
catch (OperationCanceledException) {
try {
metaExtractProc?.Kill();
}
catch {
}
}
catch {
try {
metaExtractProc?.Kill();
}
catch {
}
}
finally {
try {
metaExtractProc?.Dispose();
}
catch {
}
try {
Directory.Delete(tempDir, true);
}
catch {
}
}
}
catch {
}
// 5.Анализ имени самого архива и родительской папки на диске
try {
string fileName = Path.GetFileName(archivePath);
ExtractPasswordsFromText(fileName, AddCand);
AddCand(Path.GetFileNameWithoutExtension(archivePath));
string? parentDir = Path.GetDirectoryName(archivePath);
if (!string.IsNullOrEmpty(parentDir)) {
string dirName = Path.GetFileName(parentDir);
ExtractPasswordsFromText(dirName, AddCand);
}
}
catch {
}
return list;
}
private async Task<List<string>> GetPasswordCandidatesAsync(string archivePath, CancellationToken ct) {
var candidates = new List<string>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
void Add(string? pass) {
if (string.IsNullOrWhiteSpace(pass)) return;
pass = pass.Trim();
if (pass.Length >= 1 && pass.Length <= 64 && seen.Add(pass)) {
candidates.Add(pass);
}
}
// 1.Пароли, успешно сработавшие в текущей сессии (пользовательские или авто-подобранные)
lock (_sessionPasswords) {
foreach (var p in _sessionPasswords) Add(p);
}
// 2.Метаданные, комментарии архива, сырые бинарные строки и незашифрованные файлы (НАИВЫСШИЙ ПРИОРИТЕТ)
var metaPasswords = await ExtractArchiveMetadataPasswordsAsync(archivePath, ct);
foreach (var p in metaPasswords) Add(p);
// 3.Загруженный словарь из passwords.json (числовые ряды 1111.., пин-коды, года, комьюнити Rust)
foreach (var p in EmbeddedPasswords) Add(p);
return candidates;
}
private async Task<bool> TestArchivePasswordAsync(string archivePath, string password, CancellationToken ct, int timeoutSeconds = 4) {
Process? process = null;
try {
process = new Process();
process.StartInfo.FileName = _sevenZipExePath;
// -bso0 -bse0 -bsp0 -bd : полностью выключаем консольный вывод 7z.// Никаких пайп-буферов, мгновенная проверка кода возврата!
process.StartInfo.Arguments = $"t \"{archivePath}\" -p\"{Escape7zArg(password)}\" -ir!*.cs -ir!*.txt -y -bso0 -bse0 -bsp0 -bd";
process.StartInfo.CreateNoWindow = true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = false;
process.StartInfo.RedirectStandardError = false;
process.Start();
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
linkedCts.CancelAfter(TimeSpan.FromSeconds(timeoutSeconds));
await process.WaitForExitAsync(linkedCts.Token);
return process.ExitCode == 0;
}
catch (OperationCanceledException) {
try {
process?.Kill();
}
catch {
}
return false;
}
catch {
try {
process?.Kill();
}
catch {
}
return false;
}
finally {
try {
process?.Dispose();
}
catch {
}
}
}
private void PositionUnlockWindow(Window window) {
Dispatcher.Invoke(() => {
double screenWidth = SystemParameters.PrimaryScreenWidth;
double screenHeight = SystemParameters.PrimaryScreenHeight;
double startLeft = this.Left + this.Width - window.Width;
double startTop = this.Top + 60;
if (double.IsNaN(startLeft) || startLeft < 0 || startLeft + window.Width > screenWidth) {
startLeft = screenWidth - window.Width - 30;
}
if (double.IsNaN(startTop) || startTop < 0 || startTop + window.Height > screenHeight) {
startTop = 60;
}
int offset = (_unlockWindowCascadeIndex % 8) * 35;
window.Left = Math.Max(20, Math.Min(screenWidth - window.Width - 20, startLeft - offset));
window.Top = Math.Max(20, Math.Min(screenHeight - window.Height - 40, startTop + offset));
_unlockWindowCascadeIndex++;
}
);
}
// Фоновый подбор пароля в отдельном окне (НЕ блокирует дальнейшее сканирование ПК!)
private async Task ProcessEncryptedArchiveInBackgroundAsync(string archivePath, CancellationToken mainCt) {
var fileInfo = new FileInfo(archivePath);
if (!fileInfo.Exists) return;
var archiveCts = CancellationTokenSource.CreateLinkedTokenSource(mainCt);
lock (_activeUnlockCts) {
_activeUnlockCts.Add(archiveCts);
}
ArchiveUnlockWindow? window = null;
await Dispatcher.InvokeAsync(() => {
window = new ArchiveUnlockWindow(archivePath, archiveCts);
window.Owner = this;
PositionUnlockWindow(window);
window.Show();
}
);
Log($"[ЗАМОК
string? foundPassword = null;
try {
// Ограничиваем одновременный брутфорс максимум 2 архивами одновременно,
// чтобы не перегружать процессор и диск фоновыми вызовами 7-Zip
await _archiveUnlockSemaphore.WaitAsync(archiveCts.Token);
try {
var candidates = await GetPasswordCandidatesAsync(archivePath, archiveCts.Token);
int total = candidates.Count;
for (int i = 0; i < candidates.Count; i++) {
if (archiveCts.IsCancellationRequested) break;
// Приоритетная проверка паролей, введенных пользователем вручную в окне дешифровки!
while (window != null && window.TryDequeueManualPassword(out var manualPass)) {
Log($"[РУЧНОЙ ВВОД] Проверка введенного пользователем пароля: '{manualPass}'...", Colors.Cyan);
bool manualOk = await TestArchivePasswordAsync(archivePath, manualPass, archiveCts.Token, timeoutSeconds : 3);
if (manualOk) {
foundPassword = manualPass;
window.NotifyManualPasswordResult(manualPass, true);
break;
}
else {
window.NotifyManualPasswordResult(manualPass, false);
}
}
if (foundPassword != null) break;
string cand = candidates;
// ... [Code preview truncated due to length. Full source in attached file] ...
[/CODE]