using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; using System.Text; using System.Threading.Tasks; using AMWD.Common.Compression; namespace AMWD.Common.Tests.Compression { [TestClass] public class ArchiveHelperTest { public TestContext TestContext { get; set; } [TestMethod] public async Task ShouldCreateZipFile() { // arrange var files = new List { new() { FileName = "folder/hello.txt", Content = Encoding.UTF8.GetBytes("Hello World") }, new() { FileName = "readme.md", Content = Encoding.UTF8.GetBytes("# Readme") } }; // act byte[] zipBytes = await ArchiveHelper.CreateZip(files, TestContext.CancellationToken); // assert using var ms = new MemoryStream(zipBytes); using var zip = new ZipArchive(ms, ZipArchiveMode.Read, leaveOpen: false); Assert.HasCount(2, zip.Entries); var entry1 = zip.GetEntry("folder/hello.txt"); Assert.IsNotNull(entry1); using (var s = entry1.Open()) using (var sr = new MemoryStream()) { await s.CopyToAsync(sr, TestContext.CancellationToken); CollectionAssert.AreEqual(files[0].Content, sr.ToArray()); } var entry2 = zip.GetEntry("readme.md"); Assert.IsNotNull(entry2); using (var s = entry2.Open()) using (var sr = new MemoryStream()) { await s.CopyToAsync(sr, TestContext.CancellationToken); CollectionAssert.AreEqual(files[1].Content, sr.ToArray()); } } [TestMethod] public async Task ShouldCreateTarGzFile() { // arrange var file1 = new ArchiveFile { FileName = "a.txt", Content = Encoding.UTF8.GetBytes("Alpha"), }; file1.SetPermissions("0755"); // rwxr-xr-x var file2 = new ArchiveFile { FileName = "b.bin", Content = [1, 2, 3, 4, 5] }; file2.SetPermissions("2750"); // setgid + rwxr-x--- var files = new List { file1, file2 }; // act byte[] tarGz = await ArchiveHelper.CreateTarGz(files, cancellationToken: TestContext.CancellationToken); // decompress gzip to raw tar bytes byte[] tarBytes; using (var msIn = new MemoryStream(tarGz)) using (var gzip = new GZipStream(msIn, CompressionMode.Decompress)) using (var msOut = new MemoryStream()) { await gzip.CopyToAsync(msOut, TestContext.CancellationToken); tarBytes = msOut.ToArray(); } var entries = ParseTarEntries(tarBytes); // assert Assert.HasCount(4, entries); // 2 files + 2 headers var entry1 = entries.FirstOrDefault(e => e.Name == "a.txt"); Assert.IsNotNull(entry1); CollectionAssert.AreEqual(file1.Content, entry1.Data); Assert.AreEqual(Convert.ToInt32("0755", 8), entry1.Mode); var entry2 = entries.FirstOrDefault(e => e.Name == "b.bin"); Assert.IsNotNull(entry2); CollectionAssert.AreEqual(file2.Content, entry2.Data); Assert.AreEqual(Convert.ToInt32("2750", 8), entry2.Mode); } private sealed class TarEntryInfo { public string Name { get; set; } public int Mode { get; set; } public byte[] Data { get; set; } } /// /// Minimal TAR parser for test purposes: reads header blocks (512 bytes) and extracts /// name, mode and data for regular files. Stops on two consecutive zero blocks or empty name. /// private static List ParseTarEntries(byte[] tar) { var result = new List(); int offset = 0; int zeroBlockCount = 0; while (offset + 512 <= tar.Length) { // read header block byte[] header = new ArraySegment(tar, offset, 512).ToArray(); // check for zero block if (IsAllZero(header)) { zeroBlockCount++; if (zeroBlockCount >= 2) break; offset += 512; continue; } zeroBlockCount = 0; string name = ReadNullTerminatedAscii(header, 0, 100); if (string.IsNullOrEmpty(name)) break; string modeStr = ReadNullTerminatedAscii(header, 100, 8).Trim(); int mode = 0; if (!string.IsNullOrEmpty(modeStr)) { mode = Convert.ToInt32(modeStr, 8); } string sizeStr = ReadNullTerminatedAscii(header, 124, 12).Trim(); long size = 0; if (!string.IsNullOrEmpty(sizeStr)) { size = Convert.ToInt64(sizeStr, 8); } offset += 512; byte[] data = new byte[size > 0 ? size : 0]; if (size > 0) { Array.Copy(tar, offset, data, 0, size); } result.Add(new TarEntryInfo { Name = name, Mode = mode, Data = data }); // advance to next header (data is stored in 512-byte blocks) long dataBlockCount = (size + 511) / 512; offset += (int)(dataBlockCount * 512); } return result; } private static bool IsAllZero(byte[] block) { foreach (byte b in block) if (b != 0) return false; return true; } private static string ReadNullTerminatedAscii(byte[] src, int start, int length) { int end = start; int last = start + length; while (end < last && src[end] != 0) end++; return Encoding.ASCII.GetString(src, start, end - start); } } }