using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Text;
namespace Sim.Runner;
///
/// 写 zip 文件。文件名照中文 Windows 的习惯用 GBK 编码(资源管理器自带的解压、老版本的压缩软件都按 GBK 读,
/// 用 UTF-8 时 Windows 7 等会显示乱码),另附 Info-ZIP 的 Unicode 文件名扩展字段(0x7075),
/// 7-Zip、Linux 的 unzip 等认这个字段的软件在别的语言的系统上也能显示正确。
/// GBK 表示不了的文件名改用 UTF-8 并设置 UTF-8 标志位。
///
internal sealed class ZipWriter
{
private static readonly Encoding Gbk;
private static readonly uint[] CrcTable = new uint[256];
static ZipWriter()
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
Gbk = Encoding.GetEncoding(936, EncoderFallback.ExceptionFallback, DecoderFallback.ExceptionFallback);
for (uint n = 0; n < 256; n++)
{
var c = n;
for (var k = 0; k < 8; k++) c = (c & 1) != 0 ? 0xEDB88320 ^ (c >> 1) : c >> 1;
CrcTable[n] = c;
}
}
private sealed record Entry(byte[] Name, byte[] Extra, ushort Flags, ushort Method, uint Crc, uint CompressedSize, uint Size, uint Offset);
private readonly MemoryStream output = new();
private readonly List entries = new();
private readonly ushort dosTime;
private readonly ushort dosDate;
/// 文件的修改时间(机房电脑上解压后看到的时间)
public ZipWriter(DateTime time)
{
dosTime = (ushort)((time.Hour << 11) | (time.Minute << 5) | (time.Second / 2));
dosDate = (ushort)(((time.Year - 1980) << 9) | (time.Month << 5) | time.Day);
}
/// 添加文件,path 用 / 分隔
public void Add(string path, byte[] data)
{
byte[] name;
byte[] extra = Array.Empty();
ushort flags = 0;
try
{
name = Gbk.GetBytes(path);
var utf8 = Encoding.UTF8.GetBytes(path);
if (utf8.Length != name.Length || !utf8.AsSpan().SequenceEqual(name))
{
// Unicode Path Extra Field:版本 1、GBK 文件名的 CRC、UTF-8 文件名
extra = new byte[4 + 1 + 4 + utf8.Length];
BitConverter.TryWriteBytes(extra.AsSpan(0), (ushort)0x7075);
BitConverter.TryWriteBytes(extra.AsSpan(2), (ushort)(1 + 4 + utf8.Length));
extra[4] = 1;
BitConverter.TryWriteBytes(extra.AsSpan(5), Crc32(name));
utf8.CopyTo(extra, 9);
}
}
catch (EncoderFallbackException)
{
name = Encoding.UTF8.GetBytes(path);
flags = 1 << 11;
}
var crc = Crc32(data);
var compressed = Deflate(data);
var method = (ushort)8;
if (compressed.Length >= data.Length)
{
compressed = data;
method = 0;
}
var entry = new Entry(name, extra, flags, method, crc, (uint)compressed.Length, (uint)data.Length, (uint)output.Position);
entries.Add(entry);
using var w = new BinaryWriter(output, Encoding.UTF8, leaveOpen: true);
w.Write(0x04034b50u);
w.Write((ushort)20);
w.Write(entry.Flags);
w.Write(entry.Method);
w.Write(dosTime);
w.Write(dosDate);
w.Write(entry.Crc);
w.Write(entry.CompressedSize);
w.Write(entry.Size);
w.Write((ushort)entry.Name.Length);
w.Write((ushort)entry.Extra.Length);
w.Write(entry.Name);
w.Write(entry.Extra);
w.Write(compressed);
}
/// 写中央目录,返回整个 zip
public byte[] Finish()
{
var start = (uint)output.Position;
using (var w = new BinaryWriter(output, Encoding.UTF8, leaveOpen: true))
{
foreach (var e in entries)
{
w.Write(0x02014b50u);
w.Write((ushort)20); // 由 MS-DOS 兼容的系统生成:解压时不设置 Unix 权限
w.Write((ushort)20);
w.Write(e.Flags);
w.Write(e.Method);
w.Write(dosTime);
w.Write(dosDate);
w.Write(e.Crc);
w.Write(e.CompressedSize);
w.Write(e.Size);
w.Write((ushort)e.Name.Length);
w.Write((ushort)e.Extra.Length);
w.Write((ushort)0); // 注释长度
w.Write((ushort)0); // 磁盘号
w.Write((ushort)0); // 内部属性
w.Write(0u); // 外部属性
w.Write(e.Offset);
w.Write(e.Name);
w.Write(e.Extra);
}
var size = (uint)output.Position - start;
w.Write(0x06054b50u);
w.Write((ushort)0);
w.Write((ushort)0);
w.Write((ushort)entries.Count);
w.Write((ushort)entries.Count);
w.Write(size);
w.Write(start);
w.Write((ushort)0);
}
return output.ToArray();
}
private static byte[] Deflate(byte[] data)
{
using var stream = new MemoryStream();
using (var deflate = new DeflateStream(stream, CompressionLevel.Optimal, leaveOpen: true)) deflate.Write(data);
return stream.ToArray();
}
private static uint Crc32(byte[] data)
{
var c = 0xFFFFFFFFu;
foreach (var b in data) c = CrcTable[(c ^ b) & 0xFF] ^ (c >> 8);
return c ^ 0xFFFFFFFFu;
}
}