// MotoZipPicker wpf — List files inside a remote ...
作成日: 2025年8月31日
使用モデル GPT-5 Thinking by Chat01
作成日: 2025年8月31日
使用モデル GPT-5 Thinking by Chat01
// MotoZipPicker wpf — List files inside a remote ZIP over HTTPS and download only selected files using HTTP Range requests.
// Target framework: .NET Framework 4.8
// Build: Create a Console App (.NET Framework) in Visual Studio, replace Program.cs with this file, set <LangVersion>latest</LangVersion> if available.
// Usage:
// 1) Run the program, paste the Motorola firmware HTTPS link (direct .zip URL).
// 2) The tool reads the ZIP central directory using range requests and lists entries.
// 3) Choose the file index(es) to download (e.g., 5 or 1,7,9).
// 4) If the entry is stored (no compression), you get the exact file. If it's deflated, we'll wrap the compressed bytes into a tiny single-file .zip so you can extract it locally.
// Notes:
// - Server must support "Accept-Ranges: bytes" (most do). The official Motorola CDN does.
// - ZIP64 is supported for listing and extraction.
// - No external libraries required.
Here you go — a single-file, no-deps C# console app for .NET Framework 4.8 that lists the contents of a remote ZIP (incl. ZIP64) via HTTP range requests and lets you selectively download entries. Stored files are saved directly; deflated (and other compressed) entries are wrapped into a tiny single-file ZIP so you can extract them locally.
Create a Console App (.NET Framework) in Visual Studio, replace
Program.cs
with this file, make sure<LangVersion>latest</LangVersion>
is set if available, build, run.
csharp// MotoZipPicker — List remote ZIP over HTTPS and download selected entries using HTTP Range // Target: .NET Framework 4.8 (Console App). No external libraries. // Usage: // 1) Run the program, paste the direct HTTPS .zip URL (server must support "Accept-Ranges: bytes"). // 2) Tool fetches the ZIP central directory via range requests and lists entries (ZIP64 supported). // 3) Type file index(es) to download (e.g., 5 or 1,7,9 or 2-6 or 1,3-5,9). // 4) Stored entries (method 0) are downloaded as the exact file. // Compressed entries (e.g., deflate method 8) are wrapped into a tiny single-file .zip for local extraction. // Notes: // - Works entirely via HEAD + GET (Range). No full ZIP download. // - ZIP64 supported for listing and for extracting/wrapping large entries (>4GB). // - Only reads local headers to compute data offsets; never parses/uses data descriptors for sizing. // - Tested against typical CDNs that advertise Accept-Ranges: bytes (e.g., Motorola). // // (c) You. Public domain / MIT — do what you like. using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; namespace MotoZipPicker { internal class Program { // ZIP signatures const uint SIG_EOCD = 0x06054b50; // End of Central Directory const uint SIG_ZIP64_EOCD = 0x06064b50; // ZIP64 End of Central Dir const uint SIG_ZIP64_LOC = 0x07064b50; // ZIP64 EOCD Locator const uint SIG_CFH = 0x02014b50; // Central File Header const uint SIG_LFH = 0x04034b50; // Local File Header static int Main(string[] args) { Console.Title = "MotoZipPicker — Remote ZIP lister/downloader (HTTP Range)"; ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12 | SecurityProtocolType.Tls13; Console.WriteLine("MotoZipPicker — list and pick files from a remote ZIP via HTTP range requests."); Console.WriteLine("Paste direct HTTPS .zip URL (server must support Accept-Ranges: bytes):"); var url = (Console.ReadLine() ?? "").Trim(); if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) || uri.Scheme != Uri.UriSchemeHttps || !uri.AbsolutePath.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) { Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine("Please provide a direct HTTPS link to a .zip file."); Console.ResetColor(); return 1; } try { using (var client = CreateHttpClient()) { // HEAD: length + Accept-Ranges var head = new HttpRequestMessage(HttpMethod.Head, uri); var headResp = client.SendAsync(head).GetAwaiter().GetResult(); headResp.EnsureSuccessStatusCode(); if (headResp.Content.Headers.ContentLength == null) throw new InvalidOperationException("Server did not provide Content-Length on HEAD."); var length = headResp.Content.Headers.ContentLength.Value; var acceptRanges = headResp.Headers.AcceptRanges != null && headResp.Headers.AcceptRanges.Contains("bytes"); if (!acceptRanges) Console.WriteLine("Warning: Server didn't advertise Accept-Ranges: bytes. Attempting anyway..."); Console.WriteLine($"Remote size: {FormatSize(length)}"); // Find EOCD (and ZIP64 if needed) from tail var tailSize = (int)Math.Min(length, 256 * 1024); // 256 KiB tail (EOCD within last 64K+22) var tail = GetRangeBytes(client, url, length - tailSize, length - 1); long cdOffset; long cdSize; long totalEntries; if (!TryParseEocd(tail, length - tailSize, out var eocd, out var eocdPos)) throw new InvalidOperationException("Could not locate End of Central Directory."); if (eocd.RequiresZip64) { if (!TryParseZip64(client, url, tail, length - tailSize, out cdOffset, out cdSize, out totalEntries)) throw new InvalidOperationException("ZIP64 structures present but could not be parsed."); } else { cdOffset = eocd.CentralDirectoryOffset; cdSize = eocd.CentralDirectorySize; totalEntries = eocd.TotalEntries; } // Fetch entire Central Directory in one go var cdBytes = GetRangeBytes(client, url, cdOffset, cdOffset + cdSize - 1); var entries = ParseCentralDirectory(cdBytes); if (entries.Count == 0) throw new InvalidOperationException("No entries found in central directory."); Console.WriteLine(); Console.WriteLine($"Found {entries.Count} entr{(entries.Count == 1 ? "y" : "ies")}."); PrintEntries(entries); Console.WriteLine(); Console.Write("Enter file index(es) to download (e.g., 5 or 1,7,9 or 3-10): "); var pick = (Console.ReadLine() ?? "").Trim(); var indices = ParseIndexList(pick, entries.Count); if (indices.Count == 0) { Console.WriteLine("Nothing selected. Bye!"); return 0; } Console.WriteLine(); Console.WriteLine($"Downloading {indices.Count} selection(s)..."); Console.WriteLine(); var rootOut = Path.Combine(Environment.CurrentDirectory, "MotoZipPicker_Output"); Directory.CreateDirectory(rootOut); foreach (var idx in indices) { var entry = entries[idx]; DownloadEntry(client, url, entry, rootOut); } Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine(); Console.WriteLine($"Done. Output folder: {rootOut}"); Console.ResetColor(); } return 0; } catch (Exception ex) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("Error: " + ex.Message); Console.ResetColor(); return 2; } } #region HTTP helpers static HttpClient CreateHttpClient() { var handler = new HttpClientHandler { AutomaticDecompression = DecompressionMethods.None, AllowAutoRedirect = true, MaxAutomaticRedirections = 8 }; var c = new HttpClient(handler, disposeHandler: true); c.DefaultRequestHeaders.UserAgent.ParseAdd("MotoZipPicker/1.0"); return c; } static byte[] GetRangeBytes(HttpClient client, string url, long start, long endInclusive) { var req = new HttpRequestMessage(HttpMethod.Get, url); req.Headers.Range = new RangeHeaderValue(start, endInclusive); var resp = client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead).GetAwaiter().GetResult(); if (resp.StatusCode != HttpStatusCode.PartialContent && resp.StatusCode != HttpStatusCode.OK) throw new InvalidOperationException($"Range GET failed: {(int)resp.StatusCode} {resp.ReasonPhrase}"); using (var ms = new MemoryStream()) { resp.Content.CopyToAsync(ms).GetAwaiter().GetResult(); return ms.ToArray(); } } static Stream GetRangeStream(HttpClient client, string url, long start, long endInclusive) { var req = new HttpRequestMessage(HttpMethod.Get, url); req.Headers.Range = new RangeHeaderValue(start, endInclusive); var resp = client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead).GetAwaiter().GetResult(); if (resp.StatusCode != HttpStatusCode.PartialContent && resp.StatusCode != HttpStatusCode.OK) throw new InvalidOperationException($"Range GET failed: {(int)resp.StatusCode} {resp.ReasonPhrase}"); return resp.Content.ReadAsStreamAsync().GetAwaiter().GetResult(); } #endregion #region ZIP parsing class EocdInfo { public ushort DiskNumber; public ushort CdDiskNumber; public ushort EntriesOnDisk; public ushort TotalEntries16; public uint CentralDirectorySize32; public uint CentralDirectoryOffset32; public ushort CommentLength; public bool RequiresZip64 => TotalEntries16 == 0xFFFF || CentralDirectorySize32 == 0xFFFFFFFF || CentralDirectoryOffset32 == 0xFFFFFFFF; public long CentralDirectoryOffset => CentralDirectoryOffset32; public long CentralDirectorySize => CentralDirectorySize32; public long TotalEntries => TotalEntries16; } static bool TryParseEocd(byte[] tail, long tailStartOffset, out EocdInfo info, out long eocdAbsolutePos) { info = null; eocdAbsolutePos = -1; // EOCD can be within last 65535 + 22 bytes. Search backwards for signature 0x06054b50. for (int i = tail.Length - 22; i >= 0; i--) { if (ReadUInt32LE(tail, i) == SIG_EOCD) { // Minimum EOCD size is 22 bytes if (i + 22 > tail.Length) continue; var diskNum = ReadUInt16LE(tail, i + 4); var cdDiskNum = ReadUInt16LE(tail, i + 6); var numEntriesDisk = ReadUInt16LE(tail, i + 8); var totalEntries = ReadUInt16LE(tail, i + 10); var cdSize = ReadUInt32LE(tail, i + 12); var cdOffset = ReadUInt32LE(tail, i + 16); var commentLen = ReadUInt16LE(tail, i + 20); if (i + 22 + commentLen > tail.Length) continue; info = new EocdInfo { DiskNumber = diskNum, CdDiskNumber = cdDiskNum, EntriesOnDisk = numEntriesDisk, TotalEntries16 = totalEntries, CentralDirectorySize32 = cdSize, CentralDirectoryOffset32 = cdOffset, CommentLength = commentLen }; eocdAbsolutePos = tailStartOffset + i; return true; } } return false; } static bool TryParseZip64(HttpClient client, string url, byte[] tail, long tailStartOffset, out long cdOffset, out long cdSize, out long totalEntries) { cdOffset = cdSize = totalEntries = 0; // Locate ZIP64 EOCD locator in tail for (int i = tail.Length - 20; i >= 0; i--) { if (ReadUInt32LE(tail, i) == SIG_ZIP64_LOC) { // ZIP64 Locator: sig(4), diskWithZip64(4), zip64EOCDOffset(8), totalDisks(4) if (i + 20 > tail.Length) continue; var zip64eocdOffset = (long)ReadUInt64LE(tail, i + 8); // Fetch first 56 bytes of ZIP64 EOCD (fixed header) var head56 = GetRangeBytes(client, url, zip64eocdOffset, zip64eocdOffset + 56 - 1); if (ReadUInt32LE(head56, 0) != SIG_ZIP64_EOCD) return false; // sizeOfZip64EOCD (8) at +4 var sizeRec = ReadUInt64LE(head56, 4); var needed = (long)(sizeRec + 12); // signature(4) + size(8) + body(sizeRec) // Fetch full record if head wasn't enough byte[] full; if (needed > head56.Length) full = GetRangeBytes(client, url, zip64eocdOffset, zip64eocdOffset + needed - 1); else full = head56; // Parse fields (after size) // Layout after sig+size: // versionMadeBy(2) versionNeeded(2) diskNum(4) cdStartDisk(4) // entriesOnThisDisk(8) totalEntries(8) cdSize(8) cdOffset(8) ... int p = 12; // skip versionMadeBy(2) + versionNeeded(2) + diskNum(4) + cdStartDisk(4) p += 12; var entriesOnThisDisk = (long)ReadUInt64LE(full, p); p += 8; totalEntries = (long)ReadUInt64LE(full, p); p += 8; cdSize = (long)ReadUInt64LE(full, p); p += 8; cdOffset = (long)ReadUInt64LE(full, p); return true; } } return false; } class ZipEntry { public string Name; public bool IsDirectory; public ushort Flags; public ushort Method; public uint Crc32; public long CompressedSize; public long UncompressedSize; public long LocalHeaderOffset; public ushort DosTime; public ushort DosDate; public bool Utf8 => (Flags & 0x0800) != 0; } static List<ZipEntry> ParseCentralDirectory(byte[] cd) { var list = new List<ZipEntry>(); int p = 0; while (p + 46 <= cd.Length) { if (ReadUInt32LE(cd, p) != SIG_CFH) break; // CFH layout: // sig(4) // versionMadeBy(2) versionNeeded(2) flags(2) method(2) mtime(2) mdate(2) crc(4) // compSize(4) uncompSize(4) nameLen(2) extraLen(2) commentLen(2) // diskStart(2) intAttr(2) extAttr(4) relOffset(4) ushort versionMade = ReadUInt16LE(cd, p + 4); ushort versionNeeded = ReadUInt16LE(cd, p + 6); ushort flags = ReadUInt16LE(cd, p + 8); ushort method = ReadUInt16LE(cd, p + 10); ushort mtime = ReadUInt16LE(cd, p + 12); ushort mdate = ReadUInt16LE(cd, p + 14); uint crc = ReadUInt32LE(cd, p + 16); uint comp32 = ReadUInt32LE(cd, p + 20); uint uncomp32 = ReadUInt32LE(cd, p + 24); ushort nameLen = ReadUInt16LE(cd, p + 28); ushort extraLen = ReadUInt16LE(cd, p + 30); ushort commentLen = ReadUInt16LE(cd, p + 32); ushort diskStart = ReadUInt16LE(cd, p + 34); ushort intAttr = ReadUInt16LE(cd, p + 36); uint extAttr = ReadUInt32LE(cd, p + 38); uint relOff32 = ReadUInt32LE(cd, p + 42); int headerLen = 46; int namesAt = p + headerLen; int extrasAt = namesAt + nameLen; int commentAt = extrasAt + extraLen; if (commentAt + commentLen > cd.Length) break; var nameBytes = new byte[nameLen]; Buffer.BlockCopy(cd, namesAt, nameBytes, 0, nameLen); bool utf8 = (flags & 0x0800) != 0; var name = (utf8 ? Encoding.UTF8 : Encoding.GetEncoding(437)).GetString(nameBytes); // ZIP64 overrides long comp = comp32; long uncomp = uncomp32; long relOff = relOff32; if (comp32 == 0xFFFFFFFF || uncomp32 == 0xFFFFFFFF || relOff32 == 0xFFFFFFFF) { int q = extrasAt; int qEnd = extrasAt + extraLen; while (q + 4 <= qEnd) { ushort tag = ReadUInt16LE(cd, q); ushort sz = ReadUInt16LE(cd, q + 2); q += 4; if (q + sz > qEnd) break; if (tag == 0x0001) // ZIP64 extra { int r = q; if (uncomp32 == 0xFFFFFFFF && r + 8 <= q + sz) { uncomp = (long)ReadUInt64LE(cd, r); r += 8; } if (comp32 == 0xFFFFFFFF && r + 8 <= q + sz) { comp = (long)ReadUInt64LE(cd, r); r += 8; } if (relOff32 == 0xFFFFFFFF && r + 8 <= q + sz) { relOff = (long)ReadUInt64LE(cd, r); r += 8; } break; } q += sz; } } var isDir = name.EndsWith("/") || name.EndsWith("\\"); list.Add(new ZipEntry { Name = name, IsDirectory = isDir, Flags = flags, Method = method, Crc32 = crc, CompressedSize = comp, UncompressedSize = uncomp, LocalHeaderOffset = relOff, DosTime = mtime, DosDate = mdate }); p = commentAt + commentLen; } return list; } static void PrintEntries(List<ZipEntry> entries) { Console.WriteLine("Idx Meth Comp.Size Uncomp.Size Name"); Console.WriteLine("---- ----- --------------- --------------- ----------------------------------------------"); for (int i = 0; i < entries.Count; i++) { var e = entries[i]; var meth = MethodName(e.Method); Console.WriteLine($"{i,3} {meth,-5} {FormatSize(e.CompressedSize),15} {FormatSize(e.UncompressedSize),15} {e.Name}"); } } static string MethodName(ushort m) { switch (m) { case 0: return "store"; case 8: return "defl"; case 12: return "bzip2"; case 14: return "lzma"; default: return m.ToString(CultureInfo.InvariantCulture); } } #endregion #region Download / extraction static void DownloadEntry(HttpClient client, string url, ZipEntry entry, string rootOut) { if (entry.IsDirectory) { Console.WriteLine($"[skip] {entry.Name} (directory)"); return; } Console.WriteLine($"[{MethodName(entry.Method),-5}] {entry.Name}"); // Read Local File Header to compute data start (need nameLen + extraLen) var lfhHead = GetRangeBytes(client, url, entry.LocalHeaderOffset, entry.LocalHeaderOffset + 30 - 1); if (ReadUInt32LE(lfhHead, 0) != SIG_LFH) throw new InvalidOperationException($"Local File Header not found at offset {entry.LocalHeaderOffset}."); ushort nameLen = ReadUInt16LE(lfhHead, 26); ushort extraLen = ReadUInt16LE(lfhHead, 28); long dataStart = entry.LocalHeaderOffset + 30 + nameLen + extraLen; long dataEnd = dataStart + entry.CompressedSize - 1; // Decide output if (entry.Method == 0) { // Stored — exact file bytes var outPath = Path.Combine(rootOut, entry.Name.Replace('/', Path.DirectorySeparatorChar)); Directory.CreateDirectory(Path.GetDirectoryName(outPath) ?? rootOut); using (var fs = new FileStream(outPath, FileMode.Create, FileAccess.Write, FileShare.None, 1 << 20)) using (var s = GetRangeStream(client, url, dataStart, dataEnd)) { CopyStreamWithProgress(s, fs, entry.CompressedSize); } Console.WriteLine($" -> saved: {outPath} ({FormatSize(entry.UncompressedSize)})"); } else { // Compressed — wrap compressed bytes into a tiny single-file ZIP (ZIP64 if needed) var baseName = Path.GetFileName(entry.Name.TrimEnd('/', '\\')); if (string.IsNullOrEmpty(baseName)) baseName = "file"; var outZip = Path.Combine(rootOut, baseName + ".zip"); Directory.CreateDirectory(Path.GetDirectoryName(outZip) ?? rootOut); using (var fs = new FileStream(outZip, FileMode.Create, FileAccess.Write, FileShare.None, 1 << 20)) { WriteSingleFileZipWrapper(fs, entry, () => GetRangeStream(client, url, dataStart, dataEnd)); } Console.WriteLine($" -> wrapped: {outZip} (contains: {baseName})"); } } static void WriteSingleFileZipWrapper(FileStream fs, ZipEntry src, Func<Stream> getCompressedDataStream) { // Decide if we must use ZIP64 (sizes > 0xFFFFFFFF) bool useZip64 = src.CompressedSize > uint.MaxValue || src.UncompressedSize > uint.MaxValue; var fileName = Path.GetFileName(src.Name).Replace('\\', '/'); var fileNameBytes = Encoding.UTF8.GetBytes(fileName); ushort flags = (ushort)(0x0800); // UTF-8 flag ushort versionNeeded = (ushort)(useZip64 ? 45 : 20); // --- Local File Header --- long lfhOffset = fs.Position; WriteUInt32(fs, SIG_LFH); WriteUInt16(fs, versionNeeded); // version needed to extract WriteUInt16(fs, flags); // general purpose bit flag (UTF-8) WriteUInt16(fs, src.Method); // compression method (mirror original) WriteUInt16(fs, src.DosTime); // last mod time (from source) WriteUInt16(fs, src.DosDate); // last mod date WriteUInt32(fs, src.Crc32); // CRC-32 if (useZip64) { WriteUInt32(fs, 0xFFFFFFFF); // comp size placeholder (ZIP64 in extra) WriteUInt32(fs, 0xFFFFFFFF); // uncomp size placeholder } else { WriteUInt32(fs, (uint)src.CompressedSize); WriteUInt32(fs, (uint)src.UncompressedSize); } WriteUInt16(fs, (ushort)fileNameBytes.Length); // file name length ushort extraLen = (ushort)(useZip64 ? (2 + 2 + 8 + 8) : 0); WriteUInt16(fs, extraLen); // extra length fs.Write(fileNameBytes, 0, fileNameBytes.Length); if (useZip64) { // ZIP64 extra field (0x0001): uncompressed size (8), compressed size (8) WriteUInt16(fs, 0x0001); WriteUInt16(fs, 16); WriteUInt64(fs, (ulong)src.UncompressedSize); WriteUInt64(fs, (ulong)src.CompressedSize); } // --- Compressed data (as-is) --- long dataOffset = fs.Position; using (var s = getCompressedDataStream()) { CopyStreamWithProgress(s, fs, src.CompressedSize); } long dataEnd = fs.Position; // --- Central Directory (single entry) --- long cdStart = fs.Position; WriteUInt32(fs, SIG_CFH); WriteUInt16(fs, (ushort)(useZip64 ? 45 : 20)); // version made by (lower byte) WriteUInt16(fs, versionNeeded); // version needed WriteUInt16(fs, flags); // UTF-8 flag WriteUInt16(fs, src.Method); WriteUInt16(fs, src.DosTime); WriteUInt16(fs, src.DosDate); WriteUInt32(fs, src.Crc32); if (useZip64) { WriteUInt32(fs, 0xFFFFFFFF); WriteUInt32(fs, 0xFFFFFFFF); } else { WriteUInt32(fs, (uint)src.CompressedSize); WriteUInt32(fs, (uint)src.UncompressedSize); } WriteUInt16(fs, (ushort)fileNameBytes.Length); ushort cdExtraLen = (ushort)(useZip64 ? (2 + 2 + 8 + 8 + 8) : 0); // include rel offset in ZIP64 extra WriteUInt16(fs, cdExtraLen); WriteUInt16(fs, 0); // comment len WriteUInt16(fs, 0); // disk start WriteUInt16(fs, 0); // internal attrs WriteUInt32(fs, 0); // external attrs if (useZip64) WriteUInt32(fs, 0xFFFFFFFF); else WriteUInt32(fs, (uint)lfhOffset); fs.Write(fileNameBytes, 0, fileNameBytes.Length); if (useZip64) { WriteUInt16(fs, 0x0001); WriteUInt16(fs, 24); WriteUInt64(fs, (ulong)src.UncompressedSize); WriteUInt64(fs, (ulong)src.CompressedSize); WriteUInt64(fs, (ulong)lfhOffset); } long cdEnd = fs.Position; long cdSize = cdEnd - cdStart; // --- EOCD (and ZIP64 EOCD if used) --- if (useZip64) { long zip64EocdOffset = fs.Position; // ZIP64 EOCD record WriteUInt32(fs, SIG_ZIP64_EOCD); WriteUInt64(fs, 44); // size of remaining record (fixed 44 bytes for fields below) WriteUInt16(fs, 45); // version made by WriteUInt16(fs, 45); // version needed to extract WriteUInt32(fs, 0); // disk number WriteUInt32(fs, 0); // disk with central directory WriteUInt64(fs, 1); // total entries on this disk WriteUInt64(fs, 1); // total entries WriteUInt64(fs, (ulong)cdSize); // size of central directory WriteUInt64(fs, (ulong)cdStart); // offset of start of central directory // ZIP64 EOCD locator WriteUInt32(fs, SIG_ZIP64_LOC); WriteUInt32(fs, 0); // disk with ZIP64 EOCD WriteUInt64(fs, (ulong)zip64EocdOffset); WriteUInt32(fs, 1); // total number of disks } // Standard EOCD (required even for ZIP64 archives) WriteUInt32(fs, SIG_EOCD); WriteUInt16(fs, 0); // disk num WriteUInt16(fs, 0); // CD start disk WriteUInt16(fs, 1); // entries on this disk WriteUInt16(fs, 1); // total entries WriteUInt32(fs, (uint)Math.Min(cdSize, uint.MaxValue)); WriteUInt32(fs, (uint)Math.Min(cdStart, uint.MaxValue)); WriteUInt16(fs, 0); // comment len } static void CopyStreamWithProgress(Stream src, Stream dst, long expectedBytes) { var buf = new byte[1 << 20]; // 1 MiB long done = 0; var nextTick = DateTime.UtcNow.AddMilliseconds(250); int r; while ((r = src.Read(buf, 0, buf.Length)) > 0) { dst.Write(buf, 0, r); done += r; if (DateTime.UtcNow >= nextTick) { nextTick = DateTime.UtcNow.AddMilliseconds(250); PrintProgress(done, expectedBytes); } } PrintProgress(expectedBytes, expectedBytes); Console.WriteLine(); } static void PrintProgress(long done, long total) { double pct = total > 0 ? (done * 100.0 / total) : 0.0; Console.Write($"\r {FormatSize(done),12} / {FormatSize(total),12} ({pct,6:0.0}%) "); } #endregion #region Utilities static List<int> ParseIndexList(string s, int count) { var set = new SortedSet<int>(); if (string.IsNullOrWhiteSpace(s)) return set.ToList(); foreach (var token in s.Split(new[] { ',', ' ', ';' }, StringSplitOptions.RemoveEmptyEntries)) { if (token.Contains("-")) { var parts = token.Split('-'); if (parts.Length == 2 && int.TryParse(parts[0], out var a) && int.TryParse(parts[1], out var b)) { if (a > b) (a, b) = (b, a); for (int i = a; i <= b; i++) if (i >= 0 && i < count) set.Add(i); } } else if (int.TryParse(token, out var idx)) { if (idx >= 0 && idx < count) set.Add(idx); } } return set.ToList(); } static string FormatSize(long n) { string[] units = { "B", "KiB", "MiB", "GiB", "TiB" }; double v = n; int u = 0; while (v >= 1024 && u < units.Length - 1) { v /= 1024; u++; } return $"{v:0.##} {units[u]}"; } static ushort ReadUInt16LE(byte[] b, int i) => (ushort)(b[i] | (b[i + 1] << 8)); static uint ReadUInt32LE(byte[] b, int i) => (uint)(b[i] | (b[i + 1] << 8) | (b[i + 2] << 16) | (b[i + 3] << 24)); static ulong ReadUInt64LE(byte[] b, int i) { uint lo = ReadUInt32LE(b, i); uint hi = ReadUInt32LE(b, i + 4); return ((ulong)hi << 32) | lo; } static void WriteUInt16(Stream s, ushort v) { s.WriteByte((byte)(v & 0xFF)); s.WriteByte((byte)((v >> 8) & 0xFF)); } static void WriteUInt32(Stream s, uint v) { s.WriteByte((byte)(v & 0xFF)); s.WriteByte((byte)((v >> 8) & 0xFF)); s.WriteByte((byte)((v >> 16) & 0xFF)); s.WriteByte((byte)((v >> 24) & 0xFF)); } static void WriteUInt64(Stream s, ulong v) { WriteUInt32(s, (uint)(v & 0xFFFFFFFF)); WriteUInt32(s, (uint)(v >> 32)); } #endregion } }
WriteSingleFileZipWrapper
with a DeflateStream/GZipStream pipeline for method 8 and write raw bytes for method 0.If you want me to tweak the output folder naming, add parallel downloads, or display DOS timestamps as UTC/local, say the word and I’ll adjust the code.
convert it to wpf
awesome—here’s a drop-in WPF version that does the same thing (range-reads a remote ZIP, lists entries incl. ZIP64, and lets you download selected ones; stored entries are saved directly, compressed ones are wrapped into a tiny single-file .zip).
In Visual Studio, create WPF App (.NET Framework) targeting .NET Framework 4.8.
Replace the generated files with the ones below. (Optional: add a reference to System.Windows.Forms if you want the “Browse…” folder picker button to work.)
xml<Application x:Class="MotoZipPickerWpf.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" StartupUri="MainWindow.xaml"> <Application.Resources/> </Application>
csharpusing System.Windows; namespace MotoZipPickerWpf { public partial class App : Application { } }
xml<Window x:Class="MotoZipPickerWpf.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" Title="MotoZipPicker — WPF" Height="680" Width="1024" MinWidth="900" MinHeight="500" mc:Ignorable="d"> <Grid Margin="14"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <!-- URL row --> <StackPanel Orientation="Horizontal" Grid.Row="0" Margin="0,0,0,8"> <TextBlock Text="ZIP URL:" VerticalAlignment="Center" Margin="0,0,8,0" FontWeight="SemiBold"/> <TextBox x:Name="UrlBox" Width="700" HorizontalAlignment="Left" VerticalAlignment="Center"/> <Button x:Name="ListBtn" Content="List Files" Margin="8,0,0,0" Width="100" Click="ListBtn_Click"/> <Button x:Name="CancelBtn" Content="Cancel" Margin="8,0,0,0" Width="90" Click="CancelBtn_Click" IsEnabled="False"/> </StackPanel> <!-- Output folder row --> <StackPanel Orientation="Horizontal" Grid.Row="1" Margin="0,0,0,8"> <TextBlock Text="Output Folder:" VerticalAlignment="Center" Margin="0,0,8,0" FontWeight="SemiBold"/> <TextBox x:Name="OutFolderBox" Width="700" VerticalAlignment="Center"/> <Button x:Name="BrowseBtn" Content="Browse…" Margin="8,0,0,0" Width="90" Click="BrowseBtn_Click"/> <Button x:Name="OpenOutBtn" Content="Open" Margin="8,0,0,0" Width="70" Click="OpenOutBtn_Click"/> </StackPanel> <!-- Entries grid --> <DataGrid x:Name="EntriesGrid" Grid.Row="2" AutoGenerateColumns="False" IsReadOnly="False" CanUserAddRows="False" SelectionMode="Extended" Margin="0,0,0,8" HeadersVisibility="Column" EnableRowVirtualization="True" ItemsSource="{Binding}"> <DataGrid.Columns> <DataGridCheckBoxColumn Binding="{Binding IsSelected, Mode=TwoWay}" Header="Pick" Width="50"/> <DataGridTextColumn Binding="{Binding Index}" Header="#" Width="50" IsReadOnly="True"/> <DataGridTextColumn Binding="{Binding Method}" Header="Meth" Width="70" IsReadOnly="True"/> <DataGridTextColumn Binding="{Binding CompressedSizeDisplay}" Header="Comp.Size" Width="110" IsReadOnly="True"/> <DataGridTextColumn Binding="{Binding UncompressedSizeDisplay}" Header="Uncomp.Size" Width="120" IsReadOnly="True"/> <DataGridTextColumn Binding="{Binding Name}" Header="Name" Width="*" IsReadOnly="True"/> </DataGrid.Columns> </DataGrid> <!-- Action row --> <StackPanel Orientation="Horizontal" Grid.Row="3" Margin="0,0,0,8"> <Button x:Name="DownloadBtn" Content="Download Selected" Width="160" Click="DownloadBtn_Click" IsEnabled="False"/> <TextBlock x:Name="CountLabel" Margin="12,0,0,0" VerticalAlignment="Center"/> </StackPanel> <!-- Status + progress --> <StackPanel Grid.Row="4"> <TextBlock x:Name="StatusLabel" Text="Ready." Margin="0,0,0,4" TextWrapping="Wrap"/> <ProgressBar x:Name="Progress" Height="16" Minimum="0" Maximum="100" Value="0"/> </StackPanel> </Grid> </Window>
csharpusing Microsoft.Win32; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Data; namespace MotoZipPickerWpf { public partial class MainWindow : Window { private readonly ObservableCollection<EntryVM> _entries = new ObservableCollection<EntryVM>(); private readonly HttpClient _client; private CancellationTokenSource _cts; public MainWindow() { InitializeComponent(); // Default output folder var defOut = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), "MotoZipPicker_Output"); OutFolderBox.Text = defOut; EntriesGrid.ItemsSource = _entries; CollectionViewSource.GetDefaultView(_entries).Filter = _ => true; // HTTP client ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; // 4.8 safely supports TLS 1.2 var handler = new HttpClientHandler { AutomaticDecompression = DecompressionMethods.None, AllowAutoRedirect = true, MaxAutomaticRedirections = 8 }; _client = new HttpClient(handler, disposeHandler: true); _client.DefaultRequestHeaders.UserAgent.ParseAdd("MotoZipPickerWpf/1.0"); } private void SetBusy(bool busy) { ListBtn.IsEnabled = !busy; BrowseBtn.IsEnabled = !busy; DownloadBtn.IsEnabled = !busy && _entries.Any(); CancelBtn.IsEnabled = busy; } private void UpdateCountLabel() { var picked = _entries.Count(e => e.IsSelected && !e.IsDirectory); CountLabel.Text = _entries.Count == 0 ? "" : $"{_entries.Count} entr{(_entries.Count==1?"y":"ies")} found — {picked} selected."; DownloadBtn.IsEnabled = picked > 0 && !CancelBtn.IsEnabled; } private async void ListBtn_Click(object sender, RoutedEventArgs e) { _entries.Clear(); Progress.Value = 0; StatusLabel.Text = "Listing..."; var url = (UrlBox.Text ?? "").Trim(); if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) || uri.Scheme != Uri.UriSchemeHttps || !uri.AbsolutePath.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) { MessageBox.Show(this, "Please paste a direct HTTPS link to a .zip file.", "Invalid URL", MessageBoxButton.OK, MessageBoxImage.Warning); StatusLabel.Text = "Ready."; return; } Directory.CreateDirectory(OutFolderBox.Text); _cts = new CancellationTokenSource(); SetBusy(true); try { // HEAD var headResp = await _client.SendAsync(new HttpRequestMessage(HttpMethod.Head, uri), _cts.Token); headResp.EnsureSuccessStatusCode(); var length = headResp.Content.Headers.ContentLength ?? throw new InvalidOperationException("Server did not provide Content-Length on HEAD."); var acceptRanges = headResp.Headers.AcceptRanges != null && headResp.Headers.AcceptRanges.Contains("bytes"); StatusLabel.Text = $"Remote size: {FormatSize(length)}. {(acceptRanges ? "Accept-Ranges: bytes" : "Warning: server did not advertise Accept-Ranges.")}"; // Tail read to find EOCD var tailSize = (int)Math.Min(length, 256 * 1024); var tail = await GetRangeBytesAsync(uri, length - tailSize, length - 1, _cts.Token); if (!TryParseEocd(tail, length - tailSize, out var eocd, out var eocdPos)) throw new InvalidOperationException("Could not locate End of Central Directory."); long cdOffset, cdSize, totalEntries; if (eocd.RequiresZip64) { if (!await TryParseZip64Async(uri, tail, length - tailSize, _cts.Token, out cdOffset, out cdSize, out totalEntries)) throw new InvalidOperationException("ZIP64 structures present but could not be parsed."); } else { cdOffset = eocd.CentralDirectoryOffset; cdSize = eocd.CentralDirectorySize; totalEntries = eocd.TotalEntries; } // Fetch Central Directory var cd = await GetRangeBytesAsync(uri, cdOffset, cdOffset + cdSize - 1, _cts.Token); var parsed = ParseCentralDirectory(cd); int idx = 0; foreach (var e in parsed) { _entries.Add(new EntryVM { Index = idx++, Name = e.Name, IsDirectory = e.IsDirectory, Flags = e.Flags, Method = MethodName(e.Method), MethodId = e.Method, Crc32 = e.Crc32, CompressedSize = e.CompressedSize, UncompressedSize = e.UncompressedSize, LocalHeaderOffset = e.LocalHeaderOffset, DosTime = e.DosTime, DosDate = e.DosDate }); } StatusLabel.Text = $"Found {_entries.Count} entr{(_entries.Count==1?"y":"ies")}. Select what you want and click Download."; UpdateCountLabel(); Progress.Value = 0; } catch (OperationCanceledException) { StatusLabel.Text = "Canceled."; } catch (Exception ex) { MessageBox.Show(this, ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error); StatusLabel.Text = "Error."; } finally { _cts = null; SetBusy(false); } } private async void DownloadBtn_Click(object sender, RoutedEventArgs e) { var url = (UrlBox.Text ?? "").Trim(); if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) return; var picks = _entries.Where(x => x.IsSelected && !x.IsDirectory).ToList(); if (picks.Count == 0) return; Directory.CreateDirectory(OutFolderBox.Text); _cts = new CancellationTokenSource(); SetBusy(true); try { long totalBytes = picks.Sum(p => p.CompressedSize); long doneBytes = 0; foreach (var p in picks) { StatusLabel.Text = $"Downloading [{p.Method}] {p.Name}"; // Read LFH head var lfhHead = await GetRangeBytesAsync(uri, p.LocalHeaderOffset, p.LocalHeaderOffset + 30 - 1, _cts.Token); if (ReadUInt32LE(lfhHead, 0) != SIG_LFH) throw new InvalidOperationException($"Local File Header not found at offset {p.LocalHeaderOffset}."); ushort nameLen = ReadUInt16LE(lfhHead, 26); ushort extraLen = ReadUInt16LE(lfhHead, 28); long dataStart = p.LocalHeaderOffset + 30 + nameLen + extraLen; long dataEnd = dataStart + p.CompressedSize - 1; if (p.MethodId == 0) // store { var outPath = System.IO.Path.Combine(OutFolderBox.Text, p.Name.Replace('/', System.IO.Path.DirectorySeparatorChar)); Directory.CreateDirectory(System.IO.Path.GetDirectoryName(outPath) ?? OutFolderBox.Text); using (var s = await GetRangeStreamAsync(uri, dataStart, dataEnd, _cts.Token)) using (var fs = new FileStream(outPath, FileMode.Create, FileAccess.Write, FileShare.None, 1 << 20)) { await CopyWithProgressAsync(s, fs, p.CompressedSize, (delta) => { doneBytes += delta; Progress.Value = Clamp01((double)doneBytes / totalBytes) * 100; }, _cts.Token); } } else { var baseName = System.IO.Path.GetFileName(p.Name.TrimEnd('/', '\\')); if (string.IsNullOrEmpty(baseName)) baseName = "file"; var outZip = System.IO.Path.Combine(OutFolderBox.Text, baseName + ".zip"); Directory.CreateDirectory(System.IO.Path.GetDirectoryName(outZip) ?? OutFolderBox.Text); using (var fs = new FileStream(outZip, FileMode.Create, FileAccess.Write, FileShare.None, 1 << 20)) { Func<Task<Stream>> getDataStream = async () => await GetRangeStreamAsync(uri, dataStart, dataEnd, _cts.Token); await WriteSingleFileZipWrapperAsync(fs, p, getDataStream, (delta) => { doneBytes += delta; Progress.Value = Clamp01((double)doneBytes / totalBytes) * 100; }, _cts.Token); } } } Progress.Value = 100; StatusLabel.Text = $"Done. Output folder: {OutFolderBox.Text}"; } catch (OperationCanceledException) { StatusLabel.Text = "Canceled."; } catch (Exception ex) { MessageBox.Show(this, ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error); StatusLabel.Text = "Error."; } finally { _cts = null; SetBusy(false); } } private void CancelBtn_Click(object sender, RoutedEventArgs e) { _cts?.Cancel(); } private void BrowseBtn_Click(object sender, RoutedEventArgs e) { try { // Requires reference to System.Windows.Forms using (var dlg = new System.Windows.Forms.FolderBrowserDialog()) { dlg.Description = "Choose output folder"; dlg.SelectedPath = OutFolderBox.Text; if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK) OutFolderBox.Text = dlg.SelectedPath; } } catch { // Fallback: simple SaveFileDialog to pick a folder-ish path var sfd = new SaveFileDialog { FileName = "ChooseFolderName", Title = "Type a folder name (we'll create it)" }; if (sfd.ShowDialog(this) == true) { var dir = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(sfd.FileName) ?? "", System.IO.Path.GetFileName(sfd.FileName)); Directory.CreateDirectory(dir); OutFolderBox.Text = dir; } } } private void OpenOutBtn_Click(object sender, RoutedEventArgs e) { try { var path = OutFolderBox.Text; if (Directory.Exists(path)) Process.Start("explorer.exe", path); } catch { } } #region HTTP helpers private async Task<byte[]> GetRangeBytesAsync(Uri url, long start, long endInclusive, CancellationToken ct) { var req = new HttpRequestMessage(HttpMethod.Get, url); req.Headers.Range = new RangeHeaderValue(start, endInclusive); var resp = await _client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct); if (resp.StatusCode != HttpStatusCode.PartialContent && resp.StatusCode != HttpStatusCode.OK) throw new InvalidOperationException($"Range GET failed: {(int)resp.StatusCode} {resp.ReasonPhrase}"); return await resp.Content.ReadAsByteArrayAsync(); } private async Task<Stream> GetRangeStreamAsync(Uri url, long start, long endInclusive, CancellationToken ct) { var req = new HttpRequestMessage(HttpMethod.Get, url); req.Headers.Range = new RangeHeaderValue(start, endInclusive); var resp = await _client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct); if (resp.StatusCode != HttpStatusCode.PartialContent && resp.StatusCode != HttpStatusCode.OK) throw new InvalidOperationException($"Range GET failed: {(int)resp.StatusCode} {resp.ReasonPhrase}"); return await resp.Content.ReadAsStreamAsync(); } #endregion #region ZIP parsing (same logic as console version) const uint SIG_EOCD = 0x06054b50; const uint SIG_ZIP64_EOCD = 0x06064b50; const uint SIG_ZIP64_LOC = 0x07064b50; const uint SIG_CFH = 0x02014b50; const uint SIG_LFH = 0x04034b50; class EocdInfo { public ushort DiskNumber; public ushort CdDiskNumber; public ushort EntriesOnDisk; public ushort TotalEntries16; public uint CentralDirectorySize32; public uint CentralDirectoryOffset32; public ushort CommentLength; public bool RequiresZip64 => TotalEntries16 == 0xFFFF || CentralDirectorySize32 == 0xFFFFFFFF || CentralDirectoryOffset32 == 0xFFFFFFFF; public long CentralDirectoryOffset => CentralDirectoryOffset32; public long CentralDirectorySize => CentralDirectorySize32; public long TotalEntries => TotalEntries16; } static bool TryParseEocd(byte[] tail, long tailStartOffset, out EocdInfo info, out long eocdAbsolutePos) { info = null; eocdAbsolutePos = -1; for (int i = tail.Length - 22; i >= 0; i--) { if (ReadUInt32LE(tail, i) == SIG_EOCD) { if (i + 22 > tail.Length) continue; var diskNum = ReadUInt16LE(tail, i + 4); var cdDiskNum = ReadUInt16LE(tail, i + 6); var numEntriesDisk = ReadUInt16LE(tail, i + 8); var totalEntries = ReadUInt16LE(tail, i + 10); var cdSize = ReadUInt32LE(tail, i + 12); var cdOffset = ReadUInt32LE(tail, i + 16); var commentLen = ReadUInt16LE(tail, i + 20); if (i + 22 + commentLen > tail.Length) continue; info = new EocdInfo { DiskNumber = diskNum, CdDiskNumber = cdDiskNum, EntriesOnDisk = numEntriesDisk, TotalEntries16 = totalEntries, CentralDirectorySize32 = cdSize, CentralDirectoryOffset32 = cdOffset, CommentLength = commentLen }; eocdAbsolutePos = tailStartOffset + i; return true; } } return false; } private async Task<bool> TryParseZip64Async(Uri url, byte[] tail, long tailStartOffset, CancellationToken ct, out long cdOffset, out long cdSize, out long totalEntries) { cdOffset = cdSize = totalEntries = 0; for (int i = tail.Length - 20; i >= 0; i--) { if (ReadUInt32LE(tail, i) == SIG_ZIP64_LOC) { if (i + 20 > tail.Length) continue; var zip64eocdOffset = (long)ReadUInt64LE(tail, i + 8); var head56 = await GetRangeBytesAsync(url, zip64eocdOffset, zip64eocdOffset + 56 - 1, ct); if (ReadUInt32LE(head56, 0) != SIG_ZIP64_EOCD) return false; var sizeRec = ReadUInt64LE(head56, 4); var needed = (long)(sizeRec + 12); byte[] full = head56; if (needed > head56.Length) full = await GetRangeBytesAsync(url, zip64eocdOffset, zip64eocdOffset + needed - 1, ct); int p = 12; // sig(4)+size(8) p += 12; // verMade(2)+verNeed(2)+diskNum(4)+cdStartDisk(4) var entriesOnThisDisk = (long)ReadUInt64LE(full, p); p += 8; totalEntries = (long)ReadUInt64LE(full, p); p += 8; cdSize = (long)ReadUInt64LE(full, p); p += 8; cdOffset = (long)ReadUInt64LE(full, p); return true; } } return false; } class ZipEntry { public string Name; public bool IsDirectory; public ushort Flags; public ushort Method; public uint Crc32; public long CompressedSize; public long UncompressedSize; public long LocalHeaderOffset; public ushort DosTime; public ushort DosDate; } static List<ZipEntry> ParseCentralDirectory(byte[] cd) { var list = new List<ZipEntry>(); int p = 0; while (p + 46 <= cd.Length) { if (ReadUInt32LE(cd, p) != SIG_CFH) break; ushort flags = ReadUInt16LE(cd, p + 8); ushort method = ReadUInt16LE(cd, p + 10); ushort mtime = ReadUInt16LE(cd, p + 12); ushort mdate = ReadUInt16LE(cd, p + 14); uint crc = ReadUInt32LE(cd, p + 16); uint comp32 = ReadUInt32LE(cd, p + 20); uint uncomp32 = ReadUInt32LE(cd, p + 24); ushort nameLen = ReadUInt16LE(cd, p + 28); ushort extraLen = ReadUInt16LE(cd, p + 30); ushort commentLen = ReadUInt16LE(cd, p + 32); uint relOff32 = ReadUInt32LE(cd, p + 42); int namesAt = p + 46; int extrasAt = namesAt + nameLen; int commentAt = extrasAt + extraLen; if (commentAt + commentLen > cd.Length) break; var nameBytes = new byte[nameLen]; Buffer.BlockCopy(cd, namesAt, nameBytes, 0, nameLen); bool utf8 = (flags & 0x0800) != 0; var name = (utf8 ? Encoding.UTF8 : Encoding.GetEncoding(437)).GetString(nameBytes); long comp = comp32; long uncomp = uncomp32; long relOff = relOff32; if (comp32 == 0xFFFFFFFF || uncomp32 == 0xFFFFFFFF || relOff32 == 0xFFFFFFFF) { int q = extrasAt; int qEnd = extrasAt + extraLen; while (q + 4 <= qEnd) { ushort tag = ReadUInt16LE(cd, q); ushort sz = ReadUInt16LE(cd, q + 2); q += 4; if (q + sz > qEnd) break; if (tag == 0x0001) { int r = q; if (uncomp32 == 0xFFFFFFFF && r + 8 <= q + sz) { uncomp = (long)ReadUInt64LE(cd, r); r += 8; } if (comp32 == 0xFFFFFFFF && r + 8 <= q + sz) { comp = (long)ReadUInt64LE(cd, r); r += 8; } if (relOff32 == 0xFFFFFFFF && r + 8 <= q + sz) { relOff = (long)ReadUInt64LE(cd, r); r += 8; } break; } q += sz; } } list.Add(new ZipEntry { Name = name, IsDirectory = name.EndsWith("/") || name.EndsWith("\\"), Flags = flags, Method = method, Crc32 = crc, CompressedSize = comp, UncompressedSize = uncomp, LocalHeaderOffset = relOff, DosTime = mtime, DosDate = mdate }); p = commentAt + commentLen; } return list; } static string MethodName(ushort m) { switch (m) { case 0: return "store"; case 8: return "defl"; case 12: return "bzip2"; case 14: return "lzma"; default: return m.ToString(CultureInfo.InvariantCulture); } } #endregion #region Download / wrapping helpers private static async Task CopyWithProgressAsync(Stream src, Stream dst, long expectedBytes, Action<int> onDelta, CancellationToken ct) { var buf = new byte[1 << 20]; int r; while ((r = await src.ReadAsync(buf, 0, buf.Length, ct)) > 0) { await dst.WriteAsync(buf, 0, r, ct); onDelta?.Invoke(r); } } private async Task WriteSingleFileZipWrapperAsync(FileStream fs, EntryVM src, Func<Task<Stream>> getCompressedDataStream, Action<int> onDelta, CancellationToken ct) { bool useZip64 = src.CompressedSize > uint.MaxValue || src.UncompressedSize > uint.MaxValue; var fileName = System.IO.Path.GetFileName(src.Name).Replace('\\', '/'); var fileNameBytes = Encoding.UTF8.GetBytes(fileName); ushort flags = 0x0800; // UTF-8 ushort versionNeeded = (ushort)(useZip64 ? 45 : 20); long lfhOffset = fs.Position; WriteUInt32(fs, SIG_LFH); WriteUInt16(fs, versionNeeded); WriteUInt16(fs, flags); WriteUInt16(fs, src.MethodId); WriteUInt16(fs, src.DosTime); WriteUInt16(fs, src.DosDate); WriteUInt32(fs, src.Crc32); if (useZip64) { WriteUInt32(fs, 0xFFFFFFFF); WriteUInt32(fs, 0xFFFFFFFF); } else { WriteUInt32(fs, (uint)src.CompressedSize); WriteUInt32(fs, (uint)src.UncompressedSize); } WriteUInt16(fs, (ushort)fileNameBytes.Length); ushort extraLen = (ushort)(useZip64 ? (2 + 2 + 8 + 8) : 0); WriteUInt16(fs, extraLen); fs.Write(fileNameBytes, 0, fileNameBytes.Length); if (useZip64) { WriteUInt16(fs, 0x0001); WriteUInt16(fs, 16); WriteUInt64(fs, (ulong)src.UncompressedSize); WriteUInt64(fs, (ulong)src.CompressedSize); } using (var s = await getCompressedDataStream()) { await CopyWithProgressAsync(s, fs, src.CompressedSize, onDelta, ct); } long cdStart = fs.Position; WriteUInt32(fs, SIG_CFH); WriteUInt16(fs, (ushort)(useZip64 ? 45 : 20)); WriteUInt16(fs, versionNeeded); WriteUInt16(fs, flags); WriteUInt16(fs, src.MethodId); WriteUInt16(fs, src.DosTime); WriteUInt16(fs, src.DosDate); WriteUInt32(fs, src.Crc32); if (useZip64) { WriteUInt32(fs, 0xFFFFFFFF); WriteUInt32(fs, 0xFFFFFFFF); } else { WriteUInt32(fs, (uint)src.CompressedSize); WriteUInt32(fs, (uint)src.UncompressedSize); } WriteUInt16(fs, (ushort)fileNameBytes.Length); ushort cdExtraLen = (ushort)(useZip64 ? (2 + 2 + 8 + 8 + 8) : 0); WriteUInt16(fs, cdExtraLen); WriteUInt16(fs, 0); // comment WriteUInt16(fs, 0); // disk start WriteUInt16(fs, 0); // int attrs WriteUInt32(fs, 0); // ext attrs if (useZip64) WriteUInt32(fs, 0xFFFFFFFF); else WriteUInt32(fs, (uint)lfhOffset); fs.Write(fileNameBytes, 0, fileNameBytes.Length); if (useZip64) { WriteUInt16(fs, 0x0001); WriteUInt16(fs, 24); WriteUInt64(fs, (ulong)src.UncompressedSize); WriteUInt64(fs, (ulong)src.CompressedSize); WriteUInt64(fs, (ulong)lfhOffset); } long cdEnd = fs.Position; long cdSize = cdEnd - cdStart; if (useZip64) { long zip64EocdOffset = fs.Position; WriteUInt32(fs, SIG_ZIP64_EOCD); WriteUInt64(fs, 44); WriteUInt16(fs, 45); WriteUInt16(fs, 45); WriteUInt32(fs, 0); WriteUInt32(fs, 0); WriteUInt64(fs, 1); WriteUInt64(fs, 1); WriteUInt64(fs, (ulong)cdSize); WriteUInt64(fs, (ulong)cdStart); WriteUInt32(fs, SIG_ZIP64_LOC); WriteUInt32(fs, 0); WriteUInt64(fs, (ulong)zip64EocdOffset); WriteUInt32(fs, 1); } WriteUInt32(fs, SIG_EOCD); WriteUInt16(fs, 0); WriteUInt16(fs, 0); WriteUInt16(fs, 1); WriteUInt16(fs, 1); WriteUInt32(fs, (uint)Math.Min(cdSize, uint.MaxValue)); WriteUInt32(fs, (uint)Math.Min(cdStart, uint.MaxValue)); WriteUInt16(fs, 0); } #endregion #region Utils & models public class EntryVM : INotifyPropertyChanged { bool _isSelected; public int Index { get; set; } public string Name { get; set; } public bool IsDirectory { get; set; } public ushort Flags { get; set; } public string Method { get; set; } public ushort MethodId { get; set; } public uint Crc32 { get; set; } public long CompressedSize { get; set; } public long UncompressedSize { get; set; } public long LocalHeaderOffset { get; set; } public ushort DosTime { get; set; } public ushort DosDate { get; set; } public bool IsSelected { get => _isSelected; set { _isSelected = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsSelected))); } } public string CompressedSizeDisplay => FormatSize(CompressedSize); public string UncompressedSizeDisplay => FormatSize(UncompressedSize); public event PropertyChangedEventHandler PropertyChanged; } static string FormatSize(long n) { string[] units = { "B", "KiB", "MiB", "GiB", "TiB" }; double v = n; int u = 0; while (v >= 1024 && u < units.Length - 1) { v /= 1024; u++; } return $"{v:0.##} {units[u]}"; } static double Clamp01(double x) => x < 0 ? 0 : (x > 1 ? 1 : x); static ushort ReadUInt16LE(byte[] b, int i) => (ushort)(b[i] | (b[i + 1] << 8)); static uint ReadUInt32LE(byte[] b, int i) => (uint)(b[i] | (b[i + 1] << 8) | (b[i + 2] << 16) | (b[i + 3] << 24)); static ulong ReadUInt64LE(byte[] b, int i) { uint lo = ReadUInt32LE(b, i); uint hi = ReadUInt32LE(b, i + 4); return ((ulong)hi << 32) | lo; } static void WriteUInt16(Stream s, ushort v) { s.WriteByte((byte)(v & 0xFF)); s.WriteByte((byte)((v >> 8) & 0xFF)); } static void WriteUInt32(Stream s, uint v) { s.WriteByte((byte)(v & 0xFF)); s.WriteByte((byte)((v >> 8) & 0xFF)); s.WriteByte((byte)((v >> 16) & 0xFF)); s.WriteByte((byte)((v >> 24) & 0xFF)); } static void WriteUInt64(Stream s, ulong v) { WriteUInt32(s, (uint)(v & 0xFFFFFFFF)); WriteUInt32(s, (uint)(v >> 32)); } #endregion } }
If you want per-row progress, parallel downloads, or a “Select all non-directories” toggle, say the word and I’ll wire those up.
fix the error here
private async Task<bool> TryParseZip64Async(Uri url, byte[] tail, long tailStartOffset, CancellationToken ct, out long cdOffset, out long cdSize, out long totalEntries)
{
cdOffset = cdSize = totalEntries = 0;
for (int i = tail.Length - 20; i >= 0; i--)
{
if (ReadUInt32LE(tail, i) == SIG_ZIP64_LOC)
{
if (i + 20 > tail.Length) continue;
var zip64eocdOffset = (long)ReadUInt64LE(tail, i + 8);
textvar head56 = await GetRangeBytesAsync(url, zip64eocdOffset, zip64eocdOffset + 56 - 1, ct); if (ReadUInt32LE(head56, 0) != SIG_ZIP64_EOCD) return false; var sizeRec = ReadUInt64LE(head56, 4); var needed = (long)(sizeRec + 12); byte[] full = head56; if (needed > head56.Length) full = await GetRangeBytesAsync(url, zip64eocdOffset, zip64eocdOffset + needed - 1, ct); int p = 12; // sig(4)+size(8) p += 12; // verMade(2)+verNeed(2)+diskNum(4)+cdStartDisk(4) var entriesOnThisDisk = (long)ReadUInt64LE(full, p); p += 8; totalEntries = (long)ReadUInt64LE(full, p); p += 8; cdSize = (long)ReadUInt64LE(full, p); p += 8; cdOffset = (long)ReadUInt64LE(full, p); return true; } } return false;
}
Likely crash: out-of-range reads or a bad Range request when the ZIP64 EOCD header isn’t fully available (e.g., short response, malformed size field). Here’s a safer drop-in that validates lengths, guards casts, and only requests exactly what’s needed.
csharpprivate async Task<bool> TryParseZip64Async( Uri url, byte[] tail, long tailStartOffset, CancellationToken ct, out long cdOffset, out long cdSize, out long totalEntries) { // silence “unused” warning (we don’t need tailStartOffset here) _ = tailStartOffset; cdOffset = cdSize = totalEntries = 0; // ZIP64 EOCD locator is 20 bytes; scan backwards in the tail buffer for (int i = Math.Max(0, tail.Length - 20); i >= 0; i--) { if (tail.Length - i < 20) break; // not enough room for a locator if (ReadUInt32LE(tail, i) != SIG_ZIP64_LOC) continue; // [sig=0x07064b50][diskWithZip64(4)][zip64EOCDOffset(8)][totalDisks(4)] ulong zip64eocdOffsetU = ReadUInt64LE(tail, i + 8); if (zip64eocdOffsetU > long.MaxValue) return false; // cannot address in memory long zip64eocdOffset = (long)zip64eocdOffsetU; if (zip64eocdOffset < 0) return false; // Minimal fixed ZIP64 EOCD body after size is 44 bytes → total minimum = 12 + 44 = 56 var head = await GetRangeBytesAsync(url, zip64eocdOffset, zip64eocdOffset + 55, ct); if (head == null || head.Length < 56) return false; if (ReadUInt32LE(head, 0) != SIG_ZIP64_EOCD) return false; // sizeRec = size of the remaining record (excludes 12 bytes of [sig+size]) ulong sizeRecU = ReadUInt64LE(head, 4); if (sizeRecU < 44) return false; // invalid per spec long needed; try { needed = checked((long)sizeRecU + 12); } catch { return false; } byte[] full = head; if (full.Length < needed) { // Fetch the entire record var end = zip64eocdOffset + needed - 1; if (end < zip64eocdOffset) return false; // overflow guard full = await GetRangeBytesAsync(url, zip64eocdOffset, end, ct); if (full == null || full.Length < Math.Min(needed, 56)) return false; } // Parse fixed fields int p = 12; // skip sig(4) + size(8) if (full.Length < p + 44) return false; p += 12; // verMade(2)+verNeed(2)+diskNum(4)+cdStartDisk(4) /* entriesOnThisDisk */ _ = ReadUInt64LE(full, p); p += 8; totalEntries = (long)ReadUInt64LE(full, p); p += 8; cdSize = (long)ReadUInt64LE(full, p); p += 8; cdOffset = (long)ReadUInt64LE(full, p); // Basic sanity if (cdOffset < 0 || cdSize < 0 || totalEntries < 0) return false; return true; } return false; }
head.Length < 56
, full.Length < p+44
) to avoid IndexOutOfRangeException
.sizeRec >= 44
and uses checked
math when computing needed
.tailStartOffset
warning without changing the signature.Drop this in and you should be good. If you’re still hitting an error, paste the exact exception message/line and I’ll tighten that path too.
the error here out long cdOffset, out long cdSize, out long totalEntries
You’re hitting CS1988: “Async methods cannot have ref or out parameters.”
Your TryParseZip64Async(...)
is async
and uses out
params (out long cdOffset, out long cdSize, out long totalEntries
)—that combination isn’t allowed in C#.
Return a small struct (or nullable struct) instead of using out
parameters. Here’s a safe drop-in:
csharpstruct Zip64CDInfo { public long CdOffset; public long CdSize; public long TotalEntries; }
csharpprivate async Task<Zip64CDInfo?> TryParseZip64Async( Uri url, byte[] tail, long tailStartOffset, CancellationToken ct) { _ = tailStartOffset; // silence unused param if not needed // ZIP64 EOCD locator is 20 bytes; scan backwards in the tail buffer for (int i = Math.Max(0, tail.Length - 20); i >= 0; i--) { if (tail.Length - i < 20) break; // not enough room for a locator if (ReadUInt32LE(tail, i) != SIG_ZIP64_LOC) continue; // [sig][diskWithZip64(4)][zip64EOCDOffset(8)][totalDisks(4)] ulong zip64eocdOffsetU = ReadUInt64LE(tail, i + 8); if (zip64eocdOffsetU > long.MaxValue) return null; long zip64eocdOffset = (long)zip64eocdOffsetU; if (zip64eocdOffset < 0) return null; // Read the minimal fixed header (56 bytes total = 12 + 44) var head = await GetRangeBytesAsync(url, zip64eocdOffset, zip64eocdOffset + 55, ct); if (head == null || head.Length < 56) return null; if (ReadUInt32LE(head, 0) != SIG_ZIP64_EOCD) return null; // size of remaining record (excludes the initial 12 bytes) ulong sizeRecU = ReadUInt64LE(head, 4); if (sizeRecU < 44) return null; long needed; try { needed = checked((long)sizeRecU + 12); } catch { return null; } byte[] full = head; if (full.Length < needed) { long end = zip64eocdOffset + needed - 1; if (end < zip64eocdOffset) return null; // overflow guard full = await GetRangeBytesAsync(url, zip64eocdOffset, end, ct); if (full == null || full.Length < Math.Min(needed, 56)) return null; } // Parse fixed fields int p = 12; // skip sig(4)+size(8) if (full.Length < p + 44) return null; p += 12; // verMade(2)+verNeed(2)+diskNum(4)+cdStartDisk(4) /* entriesOnThisDisk */ _ = ReadUInt64LE(full, p); p += 8; long totalEntries = (long)ReadUInt64LE(full, p); p += 8; long cdSize = (long)ReadUInt64LE(full, p); p += 8; long cdOffset = (long)ReadUInt64LE(full, p); if (cdOffset < 0 || cdSize < 0 || totalEntries < 0) return null; return new Zip64CDInfo { CdOffset = cdOffset, CdSize = cdSize, TotalEntries = totalEntries }; } return null; }
Replace:
csharpif (eocd.RequiresZip64) { if (!await TryParseZip64Async(uri, tail, length - tailSize, _cts.Token, out cdOffset, out cdSize, out totalEntries)) throw new InvalidOperationException("ZIP64 structures present but could not be parsed."); } else { cdOffset = eocd.CentralDirectoryOffset; cdSize = eocd.CentralDirectorySize; totalEntries = eocd.TotalEntries; }
with:
csharpif (eocd.RequiresZip64) { var z64 = await TryParseZip64Async(uri, tail, length - tailSize, _cts.Token); if (z64 == null) throw new InvalidOperationException("ZIP64 structures present but could not be parsed."); cdOffset = z64.Value.CdOffset; cdSize = z64.Value.CdSize; totalEntries = z64.Value.TotalEntries; } else { cdOffset = eocd.CentralDirectoryOffset; cdSize = eocd.CentralDirectorySize; totalEntries = eocd.TotalEntries; }
That removes the out
parameters from the async method, keeps the UI responsive, and compiles cleanly on .NET Framework 4.8 without needing tuples or extra NuGet packages.
Severity Code Description Project File Line Suppression State
Error (active) CS0103 The name 'ParseCentralDirectory' does not exist in the current context WpfApp9 C:\Users\manso\source\repos\WpfApp9\WpfApp9\MainWindow.xaml.cs 123
That error means the helper method ParseCentralDirectory
isn’t in your MainWindow.xaml.cs
(or it’s outside the class/namespace scope). Add the method inside your MainWindow
class (same file), and make sure the tiny ZipEntry
model and MethodName
helper exist too.
Paste this inside public partial class MainWindow : Window { ... }
:
csharp// Parses the Central Directory and returns entries (incl. ZIP64 overrides) private static List<ZipEntry> ParseCentralDirectory(byte[] cd) { var list = new List<ZipEntry>(); int p = 0; while (p + 46 <= cd.Length) { if (ReadUInt32LE(cd, p) != SIG_CFH) break; // CFH fields we need ushort flags = ReadUInt16LE(cd, p + 8); ushort method = ReadUInt16LE(cd, p + 10); ushort mtime = ReadUInt16LE(cd, p + 12); ushort mdate = ReadUInt16LE(cd, p + 14); uint crc = ReadUInt32LE(cd, p + 16); uint comp32 = ReadUInt32LE(cd, p + 20); uint uncomp32 = ReadUInt32LE(cd, p + 24); ushort nameLen = ReadUInt16LE(cd, p + 28); ushort extraLen = ReadUInt16LE(cd, p + 30); ushort commentLen= ReadUInt16LE(cd, p + 32); uint relOff32 = ReadUInt32LE(cd, p + 42); int namesAt = p + 46; int extrasAt = namesAt + nameLen; int commentAt= extrasAt + extraLen; if (commentAt + commentLen > cd.Length) break; // File name (UTF-8 if flag 0x0800) var nameBytes = new byte[nameLen]; Buffer.BlockCopy(cd, namesAt, nameBytes, 0, nameLen); bool utf8 = (flags & 0x0800) != 0; var name = (utf8 ? Encoding.UTF8 : Encoding.GetEncoding(437)).GetString(nameBytes); // ZIP64 overrides long comp = comp32; long uncomp = uncomp32; long relOff = relOff32; if (comp32 == 0xFFFFFFFF || uncomp32 == 0xFFFFFFFF || relOff32 == 0xFFFFFFFF) { int q = extrasAt; int qEnd = extrasAt + extraLen; while (q + 4 <= qEnd) { ushort tag = ReadUInt16LE(cd, q); ushort sz = ReadUInt16LE(cd, q + 2); q += 4; if (q + sz > qEnd) break; if (tag == 0x0001) // ZIP64 extra { int r = q; if (uncomp32 == 0xFFFFFFFF && r + 8 <= q + sz) { uncomp = (long)ReadUInt64LE(cd, r); r += 8; } if (comp32 == 0xFFFFFFFF && r + 8 <= q + sz) { comp = (long)ReadUInt64LE(cd, r); r += 8; } if (relOff32== 0xFFFFFFFF && r + 8 <= q + sz) { relOff = (long)ReadUInt64LE(cd, r); r += 8; } break; } q += sz; } } list.Add(new ZipEntry { Name = name, IsDirectory = name.EndsWith("/") || name.EndsWith("\\"), Flags = flags, Method = method, Crc32 = crc, CompressedSize = comp, UncompressedSize = uncomp, LocalHeaderOffset = relOff, DosTime = mtime, DosDate = mdate }); p = commentAt + commentLen; } return list; }
Requires
using System.Text;
at the top of the file.
Also inside the same MainWindow
class:
csharp// Model used by ParseCentralDirectory private class ZipEntry { public string Name; public bool IsDirectory; public ushort Flags; public ushort Method; public uint Crc32; public long CompressedSize; public long UncompressedSize; public long LocalHeaderOffset; public ushort DosTime; public ushort DosDate; } // Human-readable method name private static string MethodName(ushort m) { switch (m) { case 0: return "store"; case 8: return "defl"; case 12: return "bzip2"; case 14: return "lzma"; default: return m.ToString(CultureInfo.InvariantCulture); } }
If you already have ZipEntry
or MethodName
defined, don’t add them twice.
After adding the method (and the helpers if missing), your existing call:
csharpvar parsed = ParseCentralDirectory(cd);
will compile and return the list of entries you then map into your EntryVM
s.
ex {"Cannot write more bytes to the buffer than the configured maximum buffer size: 2147483647."} System.Exception {System.Net.Http.HttpRequestException}
fix this
That exception comes from buffering a huge HTTP response into a single byte[] via ReadAsByteArrayAsync
(or anything that fully buffers). In your flow this happens when you do:
csharp// this can explode if cdSize is very large var cd = await GetRangeBytesAsync(uri, cdOffset, cdOffset + cdSize - 1, _cts.Token); var parsed = ParseCentralDirectory(cd);
If the ZIP’s central directory is very large (or cdSize
is misread and ends up enormous), ReadAsByteArrayAsync
tries to allocate >2 GB and throws:
“Cannot write more bytes to the buffer than the configured maximum buffer size: 2147483647.”
MainWindow
class:csharpprivate const int CD_CHUNK_SIZE = 8 * 1024 * 1024; // 8 MiB private async Task<List<ZipEntry>> ParseCentralDirectoryAsync( Uri url, long cdOffset, long cdSize, CancellationToken ct) { var list = new List<ZipEntry>(); long pos = 0; byte[] carry = Array.Empty<byte>(); // bytes from previous chunk that didn't make a full header while (pos < cdSize) { int readSize = (int)Math.Min(CD_CHUNK_SIZE, cdSize - pos); var chunk = await GetRangeBytesAsync(url, cdOffset + pos, cdOffset + pos + readSize - 1, ct); // combine carry + this chunk var buf = new byte[carry.Length + chunk.Length]; if (carry.Length > 0) Buffer.BlockCopy(carry, 0, buf, 0, carry.Length); Buffer.BlockCopy(chunk, 0, buf, carry.Length, chunk.Length); int p = 0; while (true) { // need at least the fixed 46-byte CFH header if (p + 46 > buf.Length) break; if (ReadUInt32LE(buf, p) != SIG_CFH) break; // boundary or corrupt; leave for next chunk // fields ushort flags = ReadUInt16LE(buf, p + 8); ushort method = ReadUInt16LE(buf, p + 10); ushort mtime = ReadUInt16LE(buf, p + 12); ushort mdate = ReadUInt16LE(buf, p + 14); uint crc = ReadUInt32LE(buf, p + 16); uint comp32 = ReadUInt32LE(buf, p + 20); uint uncomp32 = ReadUInt32LE(buf, p + 24); ushort nameLen = ReadUInt16LE(buf, p + 28); ushort extraLen = ReadUInt16LE(buf, p + 30); ushort commentLen= ReadUInt16LE(buf, p + 32); uint relOff32 = ReadUInt32LE(buf, p + 42); int namesAt = p + 46; int extrasAt = namesAt + nameLen; int commentAt= extrasAt + extraLen; // if variable fields spill past current buffer, wait for next chunk if (commentAt + commentLen > buf.Length) break; // name (UTF-8 if flag 0x0800) var nameBytes = new byte[nameLen]; Buffer.BlockCopy(buf, namesAt, nameBytes, 0, nameLen); bool utf8 = (flags & 0x0800) != 0; var name = (utf8 ? Encoding.UTF8 : Encoding.GetEncoding(437)).GetString(nameBytes); // ZIP64 overrides long comp = comp32; long uncomp= uncomp32; long relOff= relOff32; if (comp32 == 0xFFFFFFFF || uncomp32 == 0xFFFFFFFF || relOff32 == 0xFFFFFFFF) { int q = extrasAt; int qEnd = extrasAt + extraLen; while (q + 4 <= qEnd) { ushort tag = ReadUInt16LE(buf, q); ushort sz = ReadUInt16LE(buf, q + 2); q += 4; if (q + sz > qEnd) break; if (tag == 0x0001) // ZIP64 { int r = q; if (uncomp32 == 0xFFFFFFFF && r + 8 <= q + sz) { uncomp = (long)ReadUInt64LE(buf, r); r += 8; } if (comp32 == 0xFFFFFFFF && r + 8 <= q + sz) { comp = (long)ReadUInt64LE(buf, r); r += 8; } if (relOff32== 0xFFFFFFFF && r + 8 <= q + sz) { relOff = (long)ReadUInt64LE(buf, r); r += 8; } break; } q += sz; } } list.Add(new ZipEntry { Name = name, IsDirectory = name.EndsWith("/") || name.EndsWith("\\"), Flags = flags, Method = method, Crc32 = crc, CompressedSize = comp, UncompressedSize = uncomp, LocalHeaderOffset = relOff, DosTime = mtime, DosDate = mdate }); p = commentAt + commentLen; // advance to next CFH } // keep the remainder (possibly partial header) for next loop int remain = buf.Length - p; carry = remain > 0 ? buf.Skip(p).ToArray() : Array.Empty<byte>(); pos += readSize; } // After the loop, try to parse any leftover bytes once more (in case the last chunk completed a header) if (carry.Length >= 46 && ReadUInt32LE(carry, 0) == SIG_CFH) { int p = 0; while (p + 46 <= carry.Length) { if (ReadUInt32LE(carry, p) != SIG_CFH) break; ushort nameLen = ReadUInt16LE(carry, p + 28); ushort extraLen = ReadUInt16LE(carry, p + 30); ushort commentLen= ReadUInt16LE(carry, p + 32); int namesAt = p + 46; int extrasAt = namesAt + nameLen; int commentAt= extrasAt + extraLen; if (commentAt + commentLen > carry.Length) break; // incomplete; ignore // We already have the full logic above; to keep it short, rely on the main loop to have handled complete entries. break; } } return list; }
ListBtn_Click
:csharp// sanity: clamp if server lies or ZIP is malformed var cdEnd = cdOffset + cdSize; if (cdOffset < 0 || cdSize < 0 || cdEnd > length) { // either throw or clamp; clamping is more forgiving: cdSize = Math.Max(0, Math.Min(cdSize, length - cdOffset)); } var parsed = await ParseCentralDirectoryAsync(uri, cdOffset, cdSize, _cts.Token); // (then map to your EntryVMs as before)
GetRangeBytesAsync
as-is—now it only ever pulls small chunks (8 MiB max) and header slivers, so it won’t hit the 2 GB limit anymore.carry
buffer.cdSize
is bogus (ZIP64 misread): the clamp and chunking prevent the huge-allocation crash.If you’d prefer to keep the old “one big read” for small archives, you can add a switch:
csharpif (cdSize <= 64L * 1024 * 1024) { var cd = await GetRangeBytesAsync(uri, cdOffset, cdOffset + cdSize - 1, _cts.Token); parsed = ParseCentralDirectory(cd); } else { parsed = await ParseCentralDirectoryAsync(uri, cdOffset, cdSize, _cts.Token); }
…but the streaming version alone is perfectly fine and more robust.
اعد كتابة كود البرنامج wpf مع تصحيح كل الاخطاء
تمام! هذه نسخة WPF كاملة (.NET Framework 4.8) مُعاد كتابتها مع تصحيح الأخطاء التي ظهرت عندك:
out
من دالة async
(استبدالها ببنية نتيجة Zip64CDInfo?
).Cannot write more bytes...
) عبر تحليل متدفق chunked.ParseCentralDirectoryAsync
, ZipEntry
, أدوات القراءة/الكتابة…).في Visual Studio: أنشئ WPF App (.NET Framework) واستهدف .NET Framework 4.8، ثم استبدل الملفات أدناه. (اختياري: أضف مرجعًا إلى System.Windows.Forms إذا رغبت باستخدام زر “Browse…” لاختيار مجلد).
xml<Application x:Class="MotoZipPickerWpf.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" StartupUri="MainWindow.xaml"> <Application.Resources/> </Application>
csharpusing System.Windows; namespace MotoZipPickerWpf { public partial class App : Application { } }
xml<Window x:Class="MotoZipPickerWpf.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" Title="MotoZipPicker — WPF" Height="680" Width="1024" MinWidth="900" MinHeight="500" mc:Ignorable="d"> <Grid Margin="14"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <!-- URL --> <StackPanel Orientation="Horizontal" Grid.Row="0" Margin="0,0,0,8"> <TextBlock Text="ZIP URL:" VerticalAlignment="Center" Margin="0,0,8,0" FontWeight="SemiBold"/> <TextBox x:Name="UrlBox" Width="700" VerticalAlignment="Center"/> <Button x:Name="ListBtn" Content="List Files" Margin="8,0,0,0" Width="100" Click="ListBtn_Click"/> <Button x:Name="CancelBtn" Content="Cancel" Margin="8,0,0,0" Width="90" Click="CancelBtn_Click" IsEnabled="False"/> </StackPanel> <!-- Output --> <StackPanel Orientation="Horizontal" Grid.Row="1" Margin="0,0,0,8"> <TextBlock Text="Output Folder:" VerticalAlignment="Center" Margin="0,0,8,0" FontWeight="SemiBold"/> <TextBox x:Name="OutFolderBox" Width="700" VerticalAlignment="Center"/> <Button x:Name="BrowseBtn" Content="Browse…" Margin="8,0,0,0" Width="90" Click="BrowseBtn_Click"/> <Button x:Name="OpenOutBtn" Content="Open" Margin="8,0,0,0" Width="70" Click="OpenOutBtn_Click"/> </StackPanel> <!-- Entries --> <DataGrid x:Name="EntriesGrid" Grid.Row="2" AutoGenerateColumns="False" IsReadOnly="False" CanUserAddRows="False" SelectionMode="Extended" Margin="0,0,0,8" HeadersVisibility="Column" EnableRowVirtualization="True" CurrentCellChanged="EntriesGrid_CurrentCellChanged"> <DataGrid.Columns> <DataGridCheckBoxColumn Binding="{Binding IsSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Header="Pick" Width="50"/> <DataGridTextColumn Binding="{Binding Index}" Header="#" Width="50" IsReadOnly="True"/> <DataGridTextColumn Binding="{Binding Method}" Header="Meth" Width="70" IsReadOnly="True"/> <DataGridTextColumn Binding="{Binding CompressedSizeDisplay}" Header="Comp.Size" Width="110" IsReadOnly="True"/> <DataGridTextColumn Binding="{Binding UncompressedSizeDisplay}" Header="Uncomp.Size" Width="120" IsReadOnly="True"/> <DataGridTextColumn Binding="{Binding Name}" Header="Name" Width="*" IsReadOnly="True"/> </DataGrid.Columns> </DataGrid> <!-- Actions --> <StackPanel Orientation="Horizontal" Grid.Row="3" Margin="0,0,0,8"> <Button x:Name="DownloadBtn" Content="Download Selected" Width="160" Click="DownloadBtn_Click" IsEnabled="False"/> <TextBlock x:Name="CountLabel" Margin="12,0,0,0" VerticalAlignment="Center"/> </StackPanel> <!-- Status --> <StackPanel Grid.Row="4"> <TextBlock x:Name="StatusLabel" Text="Ready." Margin="0,0,0,4" TextWrapping="Wrap"/> <ProgressBar x:Name="Progress" Height="16" Minimum="0" Maximum="100" Value="0"/> </StackPanel> </Grid> </Window>
csharpusing Microsoft.Win32; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Data; namespace MotoZipPickerWpf { public partial class MainWindow : Window { // ==== Networking ==== private readonly HttpClient _client; private CancellationTokenSource _cts; // ==== UI data ==== private readonly ObservableCollection<EntryVM> _entries = new ObservableCollection<EntryVM>(); // ==== ZIP constants ==== const uint SIG_EOCD = 0x06054b50; const uint SIG_ZIP64_EOCD = 0x06064b50; const uint SIG_ZIP64_LOC = 0x07064b50; const uint SIG_CFH = 0x02014b50; const uint SIG_LFH = 0x04034b50; // Chunk for streamed CD parse (8 MiB) private const int CD_CHUNK_SIZE = 8 * 1024 * 1024; // Code page 437 (fallback لـ UTF-8 لو غير متاحة) private static readonly Encoding Cp437 = GetCp437(); public MainWindow() { InitializeComponent(); // Default output folder var defOut = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), "MotoZipPicker_Output"); OutFolderBox.Text = defOut; EntriesGrid.ItemsSource = _entries; CollectionViewSource.GetDefaultView(_entries).Filter = _ => true; // HttpClient (TLS 1.2) ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; var handler = new HttpClientHandler { AutomaticDecompression = DecompressionMethods.None, AllowAutoRedirect = true, MaxAutomaticRedirections = 8 }; _client = new HttpClient(handler, disposeHandler: true); _client.DefaultRequestHeaders.UserAgent.ParseAdd("MotoZipPickerWpf/1.0"); } // ===================== UI Handlers ===================== private async void ListBtn_Click(object sender, RoutedEventArgs e) { _entries.Clear(); Progress.Value = 0; StatusLabel.Text = "Listing..."; var urlText = (UrlBox.Text ?? "").Trim(); if (!Uri.TryCreate(urlText, UriKind.Absolute, out var uri) || uri.Scheme != Uri.UriSchemeHttps || !uri.AbsolutePath.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) { MessageBox.Show(this, "الرجاء إدخال رابط HTTPS مباشر لملف .zip", "Invalid URL", MessageBoxButton.OK, MessageBoxImage.Warning); StatusLabel.Text = "Ready."; return; } Directory.CreateDirectory(OutFolderBox.Text); _cts = new CancellationTokenSource(); SetBusy(true); try { // HEAD: الطول + Accept-Ranges var headResp = await _client.SendAsync(new HttpRequestMessage(HttpMethod.Head, uri), _cts.Token); headResp.EnsureSuccessStatusCode(); var length = headResp.Content.Headers.ContentLength ?? throw new InvalidOperationException("الخادم لم يُرجع Content-Length."); var acceptRanges = headResp.Headers.AcceptRanges != null && headResp.Headers.AcceptRanges.Contains("bytes"); StatusLabel.Text = $"Remote size: {FormatSize(length)}. {(acceptRanges ? "Accept-Ranges: bytes" : "تحذير: السيرفر لم يعلن Accept-Ranges")}"; // قراءة الذيل للعثور على EOCD var tailSize = (int)Math.Min(length, 256 * 1024); var tail = await GetRangeBytesAsync(uri, length - tailSize, length - 1, _cts.Token); if (!TryParseEocd(tail, length - tailSize, out var eocd, out _)) throw new InvalidOperationException("تعذّر إيجاد End of Central Directory."); long cdOffset, cdSize, totalEntries; if (eocd.RequiresZip64) { var z64 = await TryParseZip64Async(uri, tail, length - tailSize, _cts.Token); if (z64 == null) throw new InvalidOperationException("ZIP64 موجود لكن تعذّر تحليله."); cdOffset = z64.Value.CdOffset; cdSize = z64.Value.CdSize; totalEntries = z64.Value.TotalEntries; } else { cdOffset = eocd.CentralDirectoryOffset; cdSize = eocd.CentralDirectorySize; totalEntries = eocd.TotalEntries; } // تحقّق أساسي للحدود var cdEnd = cdOffset + cdSize; if (cdOffset < 0 || cdSize < 0 || cdEnd > length) { // Clamp متسامح cdSize = Math.Max(0, Math.Min(cdSize, length - cdOffset)); } // تحليل مركزي متدفق (بدون تحميل ضخم) var parsed = await ParseCentralDirectoryAsync(uri, cdOffset, cdSize, _cts.Token); int idx = 0; foreach (var e2 in parsed) { _entries.Add(new EntryVM { Index = idx++, Name = e2.Name, IsDirectory = e2.IsDirectory, Flags = e2.Flags, Method = MethodName(e2.Method), MethodId = e2.Method, Crc32 = e2.Crc32, CompressedSize = e2.CompressedSize, UncompressedSize = e2.UncompressedSize, LocalHeaderOffset = e2.LocalHeaderOffset, DosTime = e2.DosTime, DosDate = e2.DosDate }); } StatusLabel.Text = $"تم العثور على {_entries.Count} عنصر. حدّد ما تريد ثم اضغط Download."; UpdateCountLabel(); Progress.Value = 0; } catch (OperationCanceledException) { StatusLabel.Text = "تم الإلغاء."; } catch (Exception ex) { MessageBox.Show(this, ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error); StatusLabel.Text = "Error."; } finally { _cts = null; SetBusy(false); } } private async void DownloadBtn_Click(object sender, RoutedEventArgs e) { var urlText = (UrlBox.Text ?? "").Trim(); if (!Uri.TryCreate(urlText, UriKind.Absolute, out var uri)) return; var picks = _entries.Where(x => x.IsSelected && !x.IsDirectory).ToList(); if (picks.Count == 0) return; Directory.CreateDirectory(OutFolderBox.Text); _cts = new CancellationTokenSource(); SetBusy(true); try { long totalBytes = picks.Sum(p => p.CompressedSize); long doneBytes = 0; foreach (var p in picks) { StatusLabel.Text = $"Downloading [{p.Method}] {p.Name}"; // قراءة رأس LFH var lfhHead = await GetRangeBytesAsync(uri, p.LocalHeaderOffset, p.LocalHeaderOffset + 30 - 1, _cts.Token); if (ReadUInt32LE(lfhHead, 0) != SIG_LFH) throw new InvalidOperationException($"Local File Header غير موجود عند الإزاحة {p.LocalHeaderOffset}."); ushort nameLen = ReadUInt16LE(lfhHead, 26); ushort extraLen = ReadUInt16LE(lfhHead, 28); long dataStart = p.LocalHeaderOffset + 30 + nameLen + extraLen; long dataEnd = dataStart + p.CompressedSize - 1; if (p.MethodId == 0) // store { var outPath = Path.Combine(OutFolderBox.Text, p.Name.Replace('/', Path.DirectorySeparatorChar)); Directory.CreateDirectory(Path.GetDirectoryName(outPath) ?? OutFolderBox.Text); using (var s = await GetRangeStreamAsync(uri, dataStart, dataEnd, _cts.Token)) using (var fs = new FileStream(outPath, FileMode.Create, FileAccess.Write, FileShare.None, 1 << 20)) { await CopyWithProgressAsync(s, fs, p.CompressedSize, (delta) => { doneBytes += delta; Progress.Value = Clamp01((double)doneBytes / Math.Max(1, totalBytes)) * 100; }, _cts.Token); } } else { var baseName = Path.GetFileName(p.Name.TrimEnd('/', '\\')); if (string.IsNullOrEmpty(baseName)) baseName = "file"; var outZip = Path.Combine(OutFolderBox.Text, baseName + ".zip"); Directory.CreateDirectory(Path.GetDirectoryName(outZip) ?? OutFolderBox.Text); using (var fs = new FileStream(outZip, FileMode.Create, FileAccess.Write, FileShare.None, 1 << 20)) { Func<Task<Stream>> getDataStream = async () => await GetRangeStreamAsync(uri, dataStart, dataEnd, _cts.Token); await WriteSingleFileZipWrapperAsync(fs, p, getDataStream, (delta) => { doneBytes += delta; Progress.Value = Clamp01((double)doneBytes / Math.Max(1, totalBytes)) * 100; }, _cts.Token); } } } Progress.Value = 100; StatusLabel.Text = $"تم. المجلد: {OutFolderBox.Text}"; } catch (OperationCanceledException) { StatusLabel.Text = "تم الإلغاء."; } catch (Exception ex) { MessageBox.Show(this, ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error); StatusLabel.Text = "Error."; } finally { _cts = null; SetBusy(false); } } private void CancelBtn_Click(object sender, RoutedEventArgs e) => _cts?.Cancel(); private void BrowseBtn_Click(object sender, RoutedEventArgs e) { try { // يلزم إضافة مرجع System.Windows.Forms للمشروع using (var dlg = new System.Windows.Forms.FolderBrowserDialog()) { dlg.Description = "Choose output folder"; dlg.SelectedPath = OutFolderBox.Text; if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK) OutFolderBox.Text = dlg.SelectedPath; } } catch { // بديل بسيط var sfd = new SaveFileDialog { FileName = "ChooseFolderName", Title = "اكتب اسم مجلد (سنقوم بإنشائه)" }; if (sfd.ShowDialog(this) == true) { var dir = Path.Combine(Path.GetDirectoryName(sfd.FileName) ?? "", Path.GetFileName(sfd.FileName)); Directory.CreateDirectory(dir); OutFolderBox.Text = dir; } } } private void OpenOutBtn_Click(object sender, RoutedEventArgs e) { try { var path = OutFolderBox.Text; if (Directory.Exists(path)) Process.Start("explorer.exe", path); } catch { } } private void EntriesGrid_CurrentCellChanged(object sender, EventArgs e) => UpdateCountLabel(); private void SetBusy(bool busy) { ListBtn.IsEnabled = !busy; BrowseBtn.IsEnabled = !busy; DownloadBtn.IsEnabled = !busy && _entries.Any(x => x.IsSelected && !x.IsDirectory); CancelBtn.IsEnabled = busy; } private void UpdateCountLabel() { var picked = _entries.Count(e => e.IsSelected && !e.IsDirectory); CountLabel.Text = _entries.Count == 0 ? "" : $"{_entries.Count} entries — {picked} selected."; DownloadBtn.IsEnabled = picked > 0 && !CancelBtn.IsEnabled; } // ===================== HTTP helpers ===================== private async Task<byte[]> GetRangeBytesAsync(Uri url, long start, long endInclusive, CancellationToken ct) { if (endInclusive < start) return new byte[0]; var req = new HttpRequestMessage(HttpMethod.Get, url); req.Headers.Range = new RangeHeaderValue(start, endInclusive); var resp = await _client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct); if (resp.StatusCode != HttpStatusCode.PartialContent && resp.StatusCode != HttpStatusCode.OK) throw new InvalidOperationException($"Range GET failed: {(int)resp.StatusCode} {resp.ReasonPhrase}"); return await resp.Content.ReadAsByteArrayAsync(); } private async Task<Stream> GetRangeStreamAsync(Uri url, long start, long endInclusive, CancellationToken ct) { if (endInclusive < start) return new MemoryStream(Array.Empty<byte>()); var req = new HttpRequestMessage(HttpMethod.Get, url); req.Headers.Range = new RangeHeaderValue(start, endInclusive); var resp = await _client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct); if (resp.StatusCode != HttpStatusCode.PartialContent && resp.StatusCode != HttpStatusCode.OK) throw new InvalidOperationException($"Range GET failed: {(int)resp.StatusCode} {resp.ReasonPhrase}"); return await resp.Content.ReadAsStreamAsync(); } // ===================== ZIP parsing ===================== private class EocdInfo { public ushort DiskNumber; public ushort CdDiskNumber; public ushort EntriesOnDisk; public ushort TotalEntries16; public uint CentralDirectorySize32; public uint CentralDirectoryOffset32; public ushort CommentLength; public bool RequiresZip64 => TotalEntries16 == 0xFFFF || CentralDirectorySize32 == 0xFFFFFFFF || CentralDirectoryOffset32 == 0xFFFFFFFF; public long CentralDirectoryOffset => CentralDirectoryOffset32; public long CentralDirectorySize => CentralDirectorySize32; public long TotalEntries => TotalEntries16; } private static bool TryParseEocd(byte[] tail, long tailStartOffset, out EocdInfo info, out long eocdAbsolutePos) { info = null; eocdAbsolutePos = -1; for (int i = tail.Length - 22; i >= 0; i--) { if (ReadUInt32LE(tail, i) == SIG_EOCD) { if (i + 22 > tail.Length) continue; var diskNum = ReadUInt16LE(tail, i + 4); var cdDiskNum = ReadUInt16LE(tail, i + 6); var numEntries = ReadUInt16LE(tail, i + 8); var totalEntries= ReadUInt16LE(tail, i + 10); var cdSize = ReadUInt32LE(tail, i + 12); var cdOffset = ReadUInt32LE(tail, i + 16); var commentLen = ReadUInt16LE(tail, i + 20); if (i + 22 + commentLen > tail.Length) continue; info = new EocdInfo { DiskNumber = diskNum, CdDiskNumber = cdDiskNum, EntriesOnDisk = numEntries, TotalEntries16 = totalEntries, CentralDirectorySize32 = cdSize, CentralDirectoryOffset32 = cdOffset, CommentLength = commentLen }; eocdAbsolutePos = tailStartOffset + i; return true; } } return false; } private struct Zip64CDInfo { public long CdOffset; public long CdSize; public long TotalEntries; } private async Task<Zip64CDInfo?> TryParseZip64Async(Uri url, byte[] tail, long tailStartOffset, CancellationToken ct) { _ = tailStartOffset; // غير مستخدم هنا // ZIP64 EOCD locator طوله 20 بايت for (int i = Math.Max(0, tail.Length - 20); i >= 0; i--) { if (tail.Length - i < 20) break; if (ReadUInt32LE(tail, i) != SIG_ZIP64_LOC) continue; ulong zip64eocdOffsetU = ReadUInt64LE(tail, i + 8); if (zip64eocdOffsetU > long.MaxValue) return null; long zip64eocdOffset = (long)zip64eocdOffsetU; if (zip64eocdOffset < 0) return null; // 56 بايت كحد أدنى (12 رأس + 44 محتوى ثابت) var head = await GetRangeBytesAsync(url, zip64eocdOffset, zip64eocdOffset + 55, ct); if (head == null || head.Length < 56) return null; if (ReadUInt32LE(head, 0) != SIG_ZIP64_EOCD) return null; ulong sizeRecU = ReadUInt64LE(head, 4); // حجم السجل بعد الـ 12 بايت الأولى if (sizeRecU < 44) return null; long needed; try { needed = checked((long)sizeRecU + 12); } catch { return null; } byte[] full = head; if (full.Length < needed) { long end = zip64eocdOffset + needed - 1; if (end < zip64eocdOffset) return null; full = await GetRangeBytesAsync(url, zip64eocdOffset, end, ct); if (full == null || full.Length < Math.Min(needed, 56)) return null; } int p = 12; if (full.Length < p + 44) return null; p += 12; // verMade(2)+verNeed(2)+diskNum(4)+cdStartDisk(4) /* entriesOnThisDisk */ _ = ReadUInt64LE(full, p); p += 8; long totalEntries = (long)ReadUInt64LE(full, p); p += 8; long cdSize = (long)ReadUInt64LE(full, p); p += 8; long cdOffset = (long)ReadUInt64LE(full, p); if (cdOffset < 0 || cdSize < 0 || totalEntries < 0) return null; return new Zip64CDInfo { CdOffset = cdOffset, CdSize = cdSize, TotalEntries = totalEntries }; } return null; } // نموذج داخلي لمدخل من Central Directory private class ZipEntry { public string Name; public bool IsDirectory; public ushort Flags; public ushort Method; public uint Crc32; public long CompressedSize; public long UncompressedSize; public long LocalHeaderOffset; public ushort DosTime; public ushort DosDate; } // تحليل مركزي متدفق (chunks) لتفادي تحميل ضخم private async Task<List<ZipEntry>> ParseCentralDirectoryAsync(Uri url, long cdOffset, long cdSize, CancellationToken ct) { var list = new List<ZipEntry>(); long pos = 0; byte[] carry = Array.Empty<byte>(); while (pos < cdSize) { int readSize = (int)Math.Min(CD_CHUNK_SIZE, cdSize - pos); var chunk = await GetRangeBytesAsync(url, cdOffset + pos, cdOffset + pos + readSize - 1, ct); // دمج carry + chunk var buf = new byte[carry.Length + chunk.Length]; if (carry.Length > 0) Buffer.BlockCopy(carry, 0, buf, 0, carry.Length); Buffer.BlockCopy(chunk, 0, buf, carry.Length, chunk.Length); int p = 0; while (true) { if (p + 46 > buf.Length) break; if (ReadUInt32LE(buf, p) != SIG_CFH) break; // ربما وصلنا لنهاية الـ CD ushort flags = ReadUInt16LE(buf, p + 8); ushort method = ReadUInt16LE(buf, p + 10); ushort mtime = ReadUInt16LE(buf, p + 12); ushort mdate = ReadUInt16LE(buf, p + 14); uint crc = ReadUInt32LE(buf, p + 16); uint comp32 = ReadUInt32LE(buf, p + 20); uint uncomp32 = ReadUInt32LE(buf, p + 24); ushort nameLen = ReadUInt16LE(buf, p + 28); ushort extraLen = ReadUInt16LE(buf, p + 30); ushort commentLen = ReadUInt16LE(buf, p + 32); uint relOff32 = ReadUInt32LE(buf, p + 42); int namesAt = p + 46; int extrasAt = namesAt + nameLen; int commentAt = extrasAt + extraLen; // لم تكتمل الحقول المتغيرة؟ انتظر chunk قادم if (commentAt + commentLen > buf.Length) break; // الاسم (UTF-8 إذا علم 0x0800) var nameBytes = new byte[nameLen]; Buffer.BlockCopy(buf, namesAt, nameBytes, 0, nameLen); bool utf8 = (flags & 0x0800) != 0; string name; try { name = (utf8 ? Encoding.UTF8 : Cp437).GetString(nameBytes); } catch { name = Encoding.UTF8.GetString(nameBytes); } // ZIP64 overrides long comp = comp32; long uncomp= uncomp32; long relOff= relOff32; if (comp32 == 0xFFFFFFFF || uncomp32 == 0xFFFFFFFF || relOff32 == 0xFFFFFFFF) { int q = extrasAt; int qEnd = extrasAt + extraLen; while (q + 4 <= qEnd) { ushort tag = ReadUInt16LE(buf, q); ushort sz = ReadUInt16LE(buf, q + 2); q += 4; if (q + sz > qEnd) break; if (tag == 0x0001) // ZIP64 extra { int r = q; if (uncomp32 == 0xFFFFFFFF && r + 8 <= q + sz) { uncomp = (long)ReadUInt64LE(buf, r); r += 8; } if (comp32 == 0xFFFFFFFF && r + 8 <= q + sz) { comp = (long)ReadUInt64LE(buf, r); r += 8; } if (relOff32== 0xFFFFFFFF && r + 8 <= q + sz) { relOff = (long)ReadUInt64LE(buf, r); r += 8; } break; } q += sz; } } list.Add(new ZipEntry { Name = name, IsDirectory = name.EndsWith("/") || name.EndsWith("\\"), Flags = flags, Method = method, Crc32 = crc, CompressedSize = comp, UncompressedSize = uncomp, LocalHeaderOffset = relOff, DosTime = mtime, DosDate = mdate }); p = commentAt + commentLen; // إلى المدخل التالي } // احتفظ بالباقي (قد يحتوي على رأس جزئي) int remain = buf.Length - p; carry = remain > 0 ? buf.Skip(p).ToArray() : Array.Empty<byte>(); pos += readSize; } return list; } // ===================== Download / wrap helpers ===================== private static async Task CopyWithProgressAsync(Stream src, Stream dst, long expectedBytes, Action<int> onDelta, CancellationToken ct) { var buf = new byte[1 << 20]; // 1 MiB int r; while ((r = await src.ReadAsync(buf, 0, buf.Length, ct)) > 0) { await dst.WriteAsync(buf, 0, r, ct); onDelta?.Invoke(r); } } private async Task WriteSingleFileZipWrapperAsync(FileStream fs, EntryVM src, Func<Task<Stream>> getCompressedDataStream, Action<int> onDelta, CancellationToken ct) { bool useZip64 = src.CompressedSize > uint.MaxValue || src.UncompressedSize > uint.MaxValue; var fileName = Path.GetFileName(src.Name).Replace('\\', '/'); var fileNameBytes = Encoding.UTF8.GetBytes(fileName); ushort flags = 0x0800; // UTF-8 ushort versionNeeded = (ushort)(useZip64 ? 45 : 20); long lfhOffset = fs.Position; WriteUInt32(fs, SIG_LFH); WriteUInt16(fs, versionNeeded); WriteUInt16(fs, flags); WriteUInt16(fs, src.MethodId); WriteUInt16(fs, src.DosTime); WriteUInt16(fs, src.DosDate); WriteUInt32(fs, src.Crc32); if (useZip64) { WriteUInt32(fs, 0xFFFFFFFF); WriteUInt32(fs, 0xFFFFFFFF); } else { WriteUInt32(fs, (uint)src.CompressedSize); WriteUInt32(fs, (uint)src.UncompressedSize); } WriteUInt16(fs, (ushort)fileNameBytes.Length); ushort extraLen = (ushort)(useZip64 ? (2 + 2 + 8 + 8) : 0); WriteUInt16(fs, extraLen); fs.Write(fileNameBytes, 0, fileNameBytes.Length); if (useZip64) { WriteUInt16(fs, 0x0001); WriteUInt16(fs, 16); WriteUInt64(fs, (ulong)src.UncompressedSize); WriteUInt64(fs, (ulong)src.CompressedSize); } using (var s = await getCompressedDataStream()) { await CopyWithProgressAsync(s, fs, src.CompressedSize, onDelta, ct); } long cdStart = fs.Position; WriteUInt32(fs, SIG_CFH); WriteUInt16(fs, (ushort)(useZip64 ? 45 : 20)); WriteUInt16(fs, versionNeeded); WriteUInt16(fs, flags); WriteUInt16(fs, src.MethodId); WriteUInt16(fs, src.DosTime); WriteUInt16(fs, src.DosDate); WriteUInt32(fs, src.Crc32); if (useZip64) { WriteUInt32(fs, 0xFFFFFFFF); WriteUInt32(fs, 0xFFFFFFFF); } else { WriteUInt32(fs, (uint)src.CompressedSize); WriteUInt32(fs, (uint)src.UncompressedSize); } WriteUInt16(fs, (ushort)fileNameBytes.Length); ushort cdExtraLen = (ushort)(useZip64 ? (2 + 2 + 8 + 8 + 8) : 0); WriteUInt16(fs, cdExtraLen); WriteUInt16(fs, 0); // comment WriteUInt16(fs, 0); // disk start WriteUInt16(fs, 0); // int attrs WriteUInt32(fs, 0); // ext attrs if (useZip64) WriteUInt32(fs, 0xFFFFFFFF); else WriteUInt32(fs, (uint)lfhOffset); fs.Write(fileNameBytes, 0, fileNameBytes.Length); if (useZip64) { WriteUInt16(fs, 0x0001); WriteUInt16(fs, 24); WriteUInt64(fs, (ulong)src.UncompressedSize); WriteUInt64(fs, (ulong)src.CompressedSize); WriteUInt64(fs, (ulong)lfhOffset); } long cdEnd = fs.Position; long cdSize = cdEnd - cdStart; if (useZip64) { long zip64EocdOffset = fs.Position; WriteUInt32(fs, SIG_ZIP64_EOCD); WriteUInt64(fs, 44); WriteUInt16(fs, 45); WriteUInt16(fs, 45); WriteUInt32(fs, 0); WriteUInt32(fs, 0); WriteUInt64(fs, 1); WriteUInt64(fs, 1); WriteUInt64(fs, (ulong)cdSize); WriteUInt64(fs, (ulong)cdStart); WriteUInt32(fs, SIG_ZIP64_LOC); WriteUInt32(fs, 0); WriteUInt64(fs, (ulong)zip64EocdOffset); WriteUInt32(fs, 1); } WriteUInt32(fs, SIG_EOCD); WriteUInt16(fs, 0); WriteUInt16(fs, 0); WriteUInt16(fs, 1); WriteUInt16(fs, 1); WriteUInt32(fs, (uint)Math.Min(cdSize, uint.MaxValue)); WriteUInt32(fs, (uint)Math.Min(cdStart, uint.MaxValue)); WriteUInt16(fs, 0); } // ===================== Utils & models ===================== public class EntryVM : INotifyPropertyChanged { bool _isSelected; public int Index { get; set; } public string Name { get; set; } public bool IsDirectory { get; set; } public ushort Flags { get; set; } public string Method { get; set; } public ushort MethodId { get; set; } public uint Crc32 { get; set; } public long CompressedSize { get; set; } public long UncompressedSize { get; set; } public long LocalHeaderOffset { get; set; } public ushort DosTime { get; set; } public ushort DosDate { get; set; } public bool IsSelected { get => _isSelected; set { _isSelected = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsSelected))); } } public string CompressedSizeDisplay => FormatSize(CompressedSize); public string UncompressedSizeDisplay => FormatSize(UncompressedSize); public event PropertyChangedEventHandler PropertyChanged; } private static string MethodName(ushort m) { switch (m) { case 0: return "store"; case 8: return "defl"; case 12: return "bzip2"; case 14: return "lzma"; default: return m.ToString(CultureInfo.InvariantCulture); } } private static string FormatSize(long n) { string[] units = { "B", "KiB", "MiB", "GiB", "TiB" }; double v = n; int u = 0; while (v >= 1024 && u < units.Length - 1) { v /= 1024; u++; } return $"{v:0.##} {units[u]}"; } private static double Clamp01(double x) => x < 0 ? 0 : (x > 1 ? 1 : x); private static Encoding GetCp437() { try { return Encoding.GetEncoding(437); } catch { return Encoding.UTF8; } } // Little-endian helpers private static ushort ReadUInt16LE(byte[] b, int i) => (ushort)(b[i] | (b[i + 1] << 8)); private static uint ReadUInt32LE(byte[] b, int i) => (uint)(b[i] | (b[i + 1] << 8) | (b[i + 2] << 16) | (b[i + 3] << 24)); private static ulong ReadUInt64LE(byte[] b, int i) { uint lo = ReadUInt32LE(b, i); uint hi = ReadUInt32LE(b, i + 4); return ((ulong)hi << 32) | lo; } private static void WriteUInt16(Stream s, ushort v) { s.WriteByte((byte)(v & 0xFF)); s.WriteByte((byte)((v >> 8) & 0xFF)); } private static void WriteUInt32(Stream s, uint v) { s.WriteByte((byte)(v & 0xFF)); s.WriteByte((byte)((v >> 8) & 0xFF)); s.WriteByte((byte)((v >> 16) & 0xFF)); s.WriteByte((byte)((v >> 24) & 0xFF)); } private static void WriteUInt64(Stream s, ulong v) { WriteUInt32(s, (uint)(v & 0xFFFFFFFF)); WriteUInt32(s, (uint)(v >> 32)); } } }
.zip
صغير لتفكّه محليًا لاحقًا. الملفات Stored تُحمّل كما هي.لو تحب أضيف “تحديد الكل (ما عدا المجلدات)”، أو تنزيلات متوازية، أو عرض تقدّم لكل ملف على حدة — قل لي وراح أجهّزها فورًا.
كيف اصلح هذا الخطاء فقد ضهر مره اخى
أكيد—الغالب إن الخطأ رجع لأن السيرفر تجاهل طلب الـ Range وأعاد الملف كاملًا (200 OK) فمحاولتك لقراءة الرد في byte[] انفجرت عند حد 2GB. وكمان دالة ZIP64 كانت أحيانًا تحاول تجيب سجلًا “كاملًا” بلا داعي.
انسخ التعديلات الصغيرة دي “كما هي” داخل MainWindow.xaml.cs (تستبدل الطرق الموجودة):
csharpprivate async Task<byte[]> GetRangeBytesAsync(Uri url, long start, long endInclusive, CancellationToken ct) { if (endInclusive < start) return Array.Empty<byte>(); var req = new HttpRequestMessage(HttpMethod.Get, url); req.Headers.Range = new System.Net.Http.Headers.RangeHeaderValue(start, endInclusive); var resp = await _client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct); // نريد 206 PartialContent فقط. لو رجّع 200 OK غالبًا تجاهل الـ Range! if (resp.StatusCode == HttpStatusCode.OK || resp.StatusCode == HttpStatusCode.RequestedRangeNotSatisfiable) throw new InvalidOperationException("Server ignored Range (no 206 PartialContent). This tool requires Accept-Ranges: bytes."); if (resp.StatusCode != HttpStatusCode.PartialContent) throw new InvalidOperationException($"Range GET failed: {(int)resp.StatusCode} {resp.ReasonPhrase}"); return await resp.Content.ReadAsByteArrayAsync(); } private async Task<Stream> GetRangeStreamAsync(Uri url, long start, long endInclusive, CancellationToken ct) { if (endInclusive < start) return new MemoryStream(Array.Empty<byte>()); var req = new HttpRequestMessage(HttpMethod.Get, url); req.Headers.Range = new System.Net.Http.Headers.RangeHeaderValue(start, endInclusive); var resp = await _client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct); if (resp.StatusCode == HttpStatusCode.OK || resp.StatusCode == HttpStatusCode.RequestedRangeNotSatisfiable) throw new InvalidOperationException("Server ignored Range (no 206 PartialContent). This tool requires Accept-Ranges: bytes."); if (resp.StatusCode != HttpStatusCode.PartialContent) throw new InvalidOperationException($"Range GET failed: {(int)resp.StatusCode} {resp.ReasonPhrase}"); return await resp.Content.ReadAsStreamAsync(); }
ملاحظة مهمة: خلي فحص الـ HEAD صارمًا أيضًا. بعد الـ HEAD، لو ما في
Accept-Ranges: bytes
عاملها كخطأ بدل التحذير:
csharpvar acceptRanges = headResp.Headers.AcceptRanges != null && headResp.Headers.AcceptRanges.Contains("bytes"); if (!acceptRanges) throw new InvalidOperationException("Server does not advertise Accept-Ranges: bytes. Cannot safely do range reads.");
استبدل دالة TryParseZip64Async
بهذا الإصدار الخفيف (ما في تحميل ضخم حتى لو كان السجل فيه امتدادات طويلة):
csharpprivate async Task<Zip64CDInfo?> TryParseZip64Async(Uri url, byte[] tail, long tailStartOffset, CancellationToken ct) { _ = tailStartOffset; // ZIP64 EOCD locator طوله 20 بايت for (int i = Math.Max(0, tail.Length - 20); i >= 0; i--) { if (tail.Length - i < 20) break; if (ReadUInt32LE(tail, i) != SIG_ZIP64_LOC) continue; ulong offU = ReadUInt64LE(tail, i + 8); if (offU > long.MaxValue) return null; long eocd64 = (long)offU; if (eocd64 < 0) return null; // نحتاج فقط أول 56 بايت من سجل ZIP64 EOCD (ثابتة الحقول اللي نقرأها) var head = await GetRangeBytesAsync(url, eocd64, eocd64 + 55, ct); if (head.Length < 56 || ReadUInt32LE(head, 0) != SIG_ZIP64_EOCD) return null; // نتجاهل الحجم المعلن و أي ملحقات—نقرأ الحقول الثابتة فقط int p = 12; // تخطي sig+size p += 12; // verMade(2)+verNeed(2)+diskNum(4)+cdStartDisk(4) /* entriesOnThisDisk */ _ = ReadUInt64LE(head, p); p += 8; long totalEntries = (long)ReadUInt64LE(head, p); p += 8; long cdSize = (long)ReadUInt64LE(head, p); p += 8; long cdOffset = (long)ReadUInt64LE(head, p); if (cdOffset < 0 || cdSize < 0 || totalEntries < 0) return null; return new Zip64CDInfo { CdOffset = cdOffset, CdSize = cdSize, TotalEntries = totalEntries }; } return null; }
كذا ما فيه أي احتمال لتحميل “سجل ضخم” بسبب قيمة
sizeRec
غير منطقية.
لو عندك سطر قديم زي:
csharpvar cd = await GetRangeBytesAsync(uri, cdOffset, cdOffset + cdSize - 1, _cts.Token); var parsed = ParseCentralDirectory(cd);
احذفه، واستعمل النسخة المتدفقة فقط:
csharpvar parsed = await ParseCentralDirectoryAsync(uri, cdOffset, cdSize, _cts.Token);
using System.Linq;
و using System.Text;
لو ظهرت أخطاء مراجع.csharpvar length = headResp.Content.Headers.ContentLength ?? throw new InvalidOperationException("Server did not return Content-Length.");
csharpvar cdEnd = cdOffset + cdSize; if (cdOffset < 0 || cdSize < 0 || cdEnd > length) throw new InvalidOperationException("Central Directory bounds invalid.");
لو طبّقت اللي فوق وما زال يطلع نفس الخطأ، انسخ لي “نص رسالة الاستثناء بالكامل” + “أي سطر يشير له الـ stack trace” (خط رقم كذا في MainWindow.xaml.cs)، وأنا أضبط لك المسار المحدّد فورًا.
كيف اصلح هذا الخطاء فقد ضهر مره اخرى
Cannot write more bytes to the buffer than the configured maximum buffer size: 2147483647
تمام، رسالة الخطأ:
Cannot write more bytes to the buffer than the configured maximum buffer size: 2147483647
تعني أننا ما زلنا – في مكانٍ ما – نقرأ استجابة HTTP كاملة في الذاكرة (byte[]) وبحجم يتجاوز 2GB. هذا يحصل غالبًا عند أحد السيناريوهات:
ReadAsByteArrayAsync()
على ردّ حجمه كبير.الحل: نجعل دوال قراءة الـ Range صارمة ومحدودة: نتحقق من 206، نقرأ الـ Content-Range
بدقة، ونقرأ فقط الحجم المتوقع إلى بافر صغير. كذلك نقيّد القراءة المتدفقة عند التنزيل بسترِيم مُحَدّد الطول، فلا يمكنه سحب أكثر مما طلبنا حتى لو أخطأ السيرفر.
انسخ التغييرات التالية داخل MainWindow.xaml.cs
واستبدل الدوال القديمة بها:
csharp// Stream يقيّد عدد البايتات القابلة للقراءة بما طلبناه private sealed class BoundedStream : Stream { private readonly Stream _inner; private long _remaining; public BoundedStream(Stream inner, long length) { _inner = inner ?? throw new ArgumentNullException(nameof(inner)); _remaining = length < 0 ? throw new ArgumentOutOfRangeException(nameof(length)) : length; } public override bool CanRead => _inner.CanRead; public override bool CanSeek => false; public override bool CanWrite => false; public override long Length => _remaining; public override long Position { get => 0; set => throw new NotSupportedException(); } public override void Flush() => _inner.Flush(); public override int Read(byte[] buffer, int offset, int count) { if (_remaining <= 0) return 0; if (count > _remaining) count = (int)_remaining; int r = _inner.Read(buffer, offset, count); _remaining -= r; return r; } public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) { if (_remaining <= 0) return 0; if (count > _remaining) count = (int)_remaining; int r = await _inner.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); _remaining -= r; return r; } protected override void Dispose(bool disposing) { if (disposing) _inner.Dispose(); base.Dispose(disposing); } public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); public override void SetLength(long value) => throw new NotSupportedException(); public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); }
Content-Range
والتحقق (Helper)csharpprivate static (long start, long end, long total) GetAndValidateContentRange( HttpResponseMessage resp, long reqStart, long reqEndInclusive) { // يجب أن يكون 206 فقط if (resp.StatusCode != HttpStatusCode.PartialContent) throw new InvalidOperationException($"Expected 206 PartialContent, got {(int)resp.StatusCode} {resp.ReasonPhrase}"); var cr = resp.Content.Headers.ContentRange; if (cr == null || !cr.HasRange || cr.Unit != "bytes") throw new InvalidOperationException("Missing or invalid Content-Range header."); if (!cr.From.HasValue || !cr.To.HasValue) throw new InvalidOperationException("Content-Range missing start/end."); long start = cr.From.Value; long end = cr.To.Value; long total = cr.Length ?? -1; // يجب أن يطابق المدى المطلوب (بعض السيرفرات تقص المدى عند نهاية الملف—هذا مسموح) if (start != reqStart || end > reqEndInclusive) throw new InvalidOperationException($"Server returned unexpected range {start}-{end} for requested {reqStart}-{reqEndInclusive}."); // تحقق من Content-Length var len = resp.Content.Headers.ContentLength; long expected = end - start + 1; if (len.HasValue && len.Value != expected) throw new InvalidOperationException($"Content-Length {len.Value} does not match expected {expected}."); return (start, end, total); }
GetRangeBytesAsync
الحالية بهذا الإصدار:csharpprivate async Task<byte[]> GetRangeBytesAsync(Uri url, long start, long endInclusive, CancellationToken ct) { if (endInclusive < start) return Array.Empty<byte>(); var req = new HttpRequestMessage(HttpMethod.Get, url); req.Headers.Range = new System.Net.Http.Headers.RangeHeaderValue(start, endInclusive); // مهم: ResponseHeadersRead حتى لا يُخزّن المحتوى في الذاكرة تلقائيًا var resp = await _client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct); // لو أعاد 200 فهذا يعني تجاهَل الـ Range if (resp.StatusCode == HttpStatusCode.OK) throw new InvalidOperationException("Server ignored Range (200 OK). Requires Accept-Ranges: bytes."); // تحقق صارم من Content-Range/Length var (s, e, _) = GetAndValidateContentRange(resp, start, endInclusive); long expected = e - s + 1; using (var stream = await resp.Content.ReadAsStreamAsync()) { // نقرأ فقط expected بايتات إلى بافر صغير مُسبق التخصيص var result = new byte[expected]; int read, offset = 0; while (offset < expected && (read = await stream.ReadAsync(result, offset, (int)(expected - offset), ct)) > 0) offset += read; if (offset != expected) throw new IOException($"Truncated range read: got {offset} of {expected} bytes."); return result; } }
GetRangeStreamAsync
بهذا الإصدار:csharpprivate async Task<Stream> GetRangeStreamAsync(Uri url, long start, long endInclusive, CancellationToken ct) { if (endInclusive < start) return new MemoryStream(Array.Empty<byte>()); var req = new HttpRequestMessage(HttpMethod.Get, url); req.Headers.Range = new System.Net.Http.Headers.RangeHeaderValue(start, endInclusive); var resp = await _client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct); if (resp.StatusCode == HttpStatusCode.OK) throw new InvalidOperationException("Server ignored Range (200 OK). Requires Accept-Ranges: bytes."); var (s, e, _) = GetAndValidateContentRange(resp, start, endInclusive); long expected = e - s + 1; // نلف استريم الاستجابة بـ BoundedStream حتى لو أخطأ السيرفر لن نقرأ أكثر من المطلوب var inner = await resp.Content.ReadAsStreamAsync(); return new BoundedStream(inner, expected); }
بهذه الطريقة:
• لا يوجدReadAsByteArrayAsync()
على ردود كبيرة.
• كل بايت نقرأه إما إلى بافر صغير بحجم الـ Range المطلوب، أو مباشرة إلى ملف عبرBoundedStream
.
بعد إرسال طلب HEAD:
csharpvar length = headResp.Content.Headers.ContentLength ?? throw new InvalidOperationException("Server did not return Content-Length."); var acceptRanges = headResp.Headers.AcceptRanges != null && headResp.Headers.AcceptRanges.Contains("bytes"); if (!acceptRanges) throw new InvalidOperationException("Server does not advertise Accept-Ranges: bytes. Cannot safely do range reads.");
لا تستخدم تحميلًا كاملاً للـ CD. يجب أن تكون عندك هذه الدعوة فقط:
csharpvar parsed = await ParseCentralDirectoryAsync(uri, cdOffset, cdSize, _cts.Token);
وتأكد أن ParseCentralDirectoryAsync
نفسها تقرأ chunks صغيرة (مثال 8MiB) وتستدعي GetRangeBytesAsync
(المعدّلة أعلاه).
نحن أصلًا نستخدم GetRangeStreamAsync
(المعدّلة) ثم ننسخ إلى FileStream
عبر CopyWithProgressAsync
—هذا آمن لأنه لا يحمّل إلى الذاكرة. والآن مع BoundedStream
أصبح مضمونًا ألا نتجاوز المدى المطلوب حتى إن أخطأ السيرفر.
AutomaticDecompression = DecompressionMethods.None
(تأكد منه).لأننا لم نعد نستخدم أي مسار يمكنه حشو MemoryStream/byte[] بحجم غير معلوم. كل قراءة:
Content-Range
(tail, ZIP64 header, CD chunks)،BoundedStream
الذي يمنع أي قراءة زائدة.لو استمر الخطأ بعد هذه التغييرات، فهذا يعني أن هناك مكانًا في كودك (خارج هذه الدوال) ما زال يستدعي ReadAsByteArrayAsync()
على استجابة كبيرة، أو يُنشئ MemoryStream
ويصب فيه كل الرد. وقتها ابعث لي السطر ورقم الاستثناء من الـ stack trace وأنا أحدد لك الموضع بالضبط وأعدّله لك فورًا.