using System.Buffers.Binary;
using System.Text;
namespace Sunfish.Mac;
/// <summary>
/// BinHex 4.0 (.hqx): texto de 7 bits con un alfabeto de 64 caracteres, compresion RLE (0x90) y
/// tres CRC (cabecera, data fork, resource fork). Pensado para ficheros moderados: se trabaja en
/// memoria (el limite lo pone quien llama).
/// </summary>
public static class BinHex
{
public const string Banner = "(This file must be converted with BinHex 4.0)";
private const string Alphabet = "!\"#$%&'()*+,-012345689@ABCDEFGHIJKLMNPQRSTUVXYZ[`abcdefhijklmpqr";
private const byte RunMarker = 0x90;
private static readonly sbyte[] Reverse = BuildReverse();
private static sbyte[] BuildReverse()
{
var table = new sbyte[128];
Array.Fill(table, (sbyte)-1);
for (int i = 0; i < Alphabet.Length; i++)
table[Alphabet[i]] = (sbyte)i;
return table;
}
/// <summary>El texto parece BinHex 4.0 (tiene la linea de aviso).</summary>
public static bool LooksLikeBinHex(ReadOnlySpan<byte> start) =>
Encoding.ASCII.GetString(start).Contains(Banner, StringComparison.Ordinal);
/// <summary>Decodifica; devuelve el fichero y su data fork, o null si no es BinHex valido (CRC incluidos).</summary>
public static (MacFile File, byte[] DataFork)? Decode(byte[] text)
{
string s = Encoding.Latin1.GetString(text);
int banner = s.IndexOf(Banner, StringComparison.Ordinal);
if (banner < 0)
return null;
int start = s.IndexOf(':', banner + Banner.Length);
if (start < 0)
return null;
int end = s.IndexOf(':', start + 1);
if (end < 0)
return null;
// 6 bits por caracter; los saltos de linea y espacios se ignoran.
var packed = new List<byte>(end - start);
int acc = 0, bits = 0;
for (int i = start + 1; i < end; i++)
{
char c = s[i];
if (c is '\r' or '\n' or ' ' or '\t')
continue;
if (c >= 128 || Reverse[c] < 0)
return null;
acc = (acc << 6) | (byte)Reverse[c];
bits += 6;
if (bits >= 8)
{
bits -= 8;
packed.Add((byte)(acc >> bits));
acc &= (1 << bits) - 1;
}
}
var data = Unrun(packed);
int p = 0;
if (data.Count < 1)
return null;
int nameLength = data[p++];
int headerLength = 1 + nameLength + 1 + 4 + 4 + 2 + 4 + 4;
if (data.Count < headerLength + 2)
return null;
var span = System.Runtime.InteropServices.CollectionsMarshal.AsSpan(data);
if (Crc(span[..headerLength]) != BinaryPrimitives.ReadUInt16BigEndian(span[headerLength..]))
return null;
var file = new MacFile { Name = Encodings.MacRoman.GetString(span.Slice(1, nameLength)) };
p = 1 + nameLength + 1; // version
span.Slice(p, 8).CopyTo(file.FinderInfo);
p += 8;
file.FinderFlags = BinaryPrimitives.ReadUInt16BigEndian(span[p..]);
p += 2;
long dataLength = BinaryPrimitives.ReadUInt32BigEndian(span[p..]);
long resourceLength = BinaryPrimitives.ReadUInt32BigEndian(span[(p + 4)..]);
p += 8 + 2;
if (p + dataLength + 2 + resourceLength + 2 > data.Count)
return null;
var dataFork = span.Slice(p, (int)dataLength).ToArray();
if (Crc(dataFork) != BinaryPrimitives.ReadUInt16BigEndian(span[(p + (int)dataLength)..]))
return null;
p += (int)dataLength + 2;
file.ResourceFork = span.Slice(p, (int)resourceLength).ToArray();
if (Crc(file.ResourceFork) != BinaryPrimitives.ReadUInt16BigEndian(span[(p + (int)resourceLength)..]))
return null;
return (file, dataFork);
}
public static byte[] Encode(MacFile file, ReadOnlySpan<byte> dataFork)
{
var name = Encodings.MacRoman.GetBytes(MacNames.Shorten(file.Name, 63));
var raw = new List<byte>(dataFork.Length + file.ResourceFork.Length + 128) { (byte)name.Length };
raw.AddRange(name);
raw.Add(0);
raw.AddRange(file.FinderInfo.AsSpan(0, 8).ToArray());
AddU16(raw, file.FinderFlags);
AddU32(raw, (uint)dataFork.Length);
AddU32(raw, (uint)file.ResourceFork.Length);
AddU16(raw, Crc(System.Runtime.InteropServices.CollectionsMarshal.AsSpan(raw)));
raw.AddRange(dataFork.ToArray());
AddU16(raw, Crc(dataFork));
raw.AddRange(file.ResourceFork);
AddU16(raw, Crc(file.ResourceFork));
var run = Run(raw);
var sb = new StringBuilder(Banner).Append("\r\n\r\n:");
int column = 1, acc = 0, bits = 0;
void Emit(int value)
{
sb.Append(Alphabet[value]);
if (++column == 64)
{
sb.Append("\r\n");
column = 0;
}
}
foreach (byte b in run)
{
acc = (acc << 8) | b;
bits += 8;
while (bits >= 6)
{
bits -= 6;
Emit((acc >> bits) & 0x3F);
}
acc &= (1 << bits) - 1;
}
if (bits > 0)
Emit((acc << (6 - bits)) & 0x3F);
sb.Append(":\r\n");
return Encoding.ASCII.GetBytes(sb.ToString());
}
/// <summary>CRC de BinHex: CRC-16/XMODEM del bloque seguido de dos bytes a cero.</summary>
internal static ushort Crc(ReadOnlySpan<byte> data) => Crc16.Compute([0, 0], Crc16.Compute(data));
/// <summary>RLE de BinHex: 0x90 n repite n-1 veces el byte anterior; 0x90 0x00 es un 0x90 literal.</summary>
private static List<byte> Unrun(List<byte> input)
{
var output = new List<byte>(input.Count * 2);
for (int i = 0; i < input.Count; i++)
{
byte b = input[i];
if (b != RunMarker)
{
output.Add(b);
continue;
}
if (++i >= input.Count)
break;
int count = input[i];
if (count == 0)
output.Add(RunMarker);
else if (output.Count > 0)
for (int k = 1; k < count; k++)
output.Add(output[^1]);
}
return output;
}
private static List<byte> Run(List<byte> input)
{
var output = new List<byte>(input.Count + input.Count / 10);
int i = 0;
while (i < input.Count)
{
byte b = input[i];
int n = 1;
while (i + n < input.Count && input[i + n] == b && n < 255)
n++;
output.Add(b);
if (b == RunMarker)
output.Add(0);
if (n > 2)
{
output.Add(RunMarker);
output.Add((byte)n);
}
else if (n == 2)
{
output.Add(b);
if (b == RunMarker)
output.Add(0);
}
i += n;
}
return output;
}
private static void AddU16(List<byte> b, ushort v)
{
b.Add((byte)(v >> 8));
b.Add((byte)v);
}
private static void AddU32(List<byte> b, uint v)
{
AddU16(b, (ushort)(v >> 16));
AddU16(b, (ushort)v);
}
}