using System.Buffers.Binary;
namespace Sunfish.Mac;
/// <summary>
/// MacBinary I, II y III: cabecera de 128 bytes, data fork y resource fork, cada uno rellenado
/// hasta multiplo de 128. Se lee y se escribe en flujo: el data fork puede ser muy grande.
/// </summary>
public static class MacBinary
{
public const int HeaderSize = 128;
public const int MaxResourceFork = 64 * 1024 * 1024;
public enum Kind { None, MacBinary1, MacBinary2, MacBinary3 }
/// <summary>Cabecera ya interpretada.</summary>
public sealed record Header(MacFile File, long DataLength, long ResourceLength, int SecondaryHeader, int CommentLength, Kind Kind);
/// <summary>
/// Reconoce una cabecera MacBinary. II y III llevan CRC y la deteccion es fiable; MacBinary I
/// no tiene CRC y solo se acepta si se pide (<paramref name="acceptVersion1"/>) y todo cuadra.
/// </summary>
public static Header? Parse(ReadOnlySpan<byte> h, long totalLength, bool acceptVersion1)
{
if (h.Length < HeaderSize || h[0] != 0 || h[74] != 0)
return null;
int nameLength = h[1];
if (nameLength is < 1 or > 63)
return null;
long dataLength = BinaryPrimitives.ReadUInt32BigEndian(h[83..]);
long resourceLength = BinaryPrimitives.ReadUInt32BigEndian(h[87..]);
int secondary = BinaryPrimitives.ReadUInt16BigEndian(h[120..]);
if (resourceLength > MaxResourceFork)
return null;
Kind kind;
ushort storedCrc = BinaryPrimitives.ReadUInt16BigEndian(h[124..]);
if (h[82] == 0 && storedCrc == Crc16.Compute(h[..124]) && storedCrc != 0)
kind = h[102] == 'm' && h[103] == 'B' && h[104] == 'I' && h[105] == 'N' ? Kind.MacBinary3 : Kind.MacBinary2;
else if (acceptVersion1 && h[82] == 0 && h[101] == 0 && h[122] == 0 && h[123] == 0 && storedCrc == 0)
kind = Kind.MacBinary1;
else
return null;
int commentLength = kind == Kind.MacBinary1 ? 0 : BinaryPrimitives.ReadUInt16BigEndian(h[99..]);
long expected = HeaderSize + Pad(secondary) + Pad(dataLength) + Pad(resourceLength);
// El ultimo bloque puede venir sin relleno; el comentario de Get Info, si lo hay, va al final.
long minimum = HeaderSize + Pad(secondary) + Pad(dataLength) + resourceLength;
if (resourceLength == 0)
minimum = HeaderSize + Pad(secondary) + dataLength;
if (totalLength < minimum || totalLength > expected + Pad(commentLength) + 128)
return null;
var file = new MacFile { Name = Encodings.MacRoman.GetString(h.Slice(2, nameLength)) };
h.Slice(65, 8).CopyTo(file.FinderInfo); // tipo y creador
file.FinderInfo[8] = h[73]; // indicadores del Finder (byte alto)
file.FinderInfo[9] = kind == Kind.MacBinary1 ? (byte)0 : h[101]; // (byte bajo, II+)
h.Slice(75, 4).CopyTo(file.FinderInfo.AsSpan(10)); // posicion en la ventana
h.Slice(79, 2).CopyTo(file.FinderInfo.AsSpan(14)); // carpeta
if (kind == Kind.MacBinary3)
{
file.FinderInfo[24] = h[106]; // fdScript
file.FinderInfo[25] = h[107]; // fdXFlags
}
file.Created = MacFile.FromMacSeconds(BinaryPrimitives.ReadUInt32BigEndian(h[91..]));
file.Modified = MacFile.FromMacSeconds(BinaryPrimitives.ReadUInt32BigEndian(h[95..]));
return new Header(file, dataLength, resourceLength, secondary, commentLength, kind);
}
/// <summary>
/// Separa un MacBinary: el data fork va a <paramref name="dataFork"/> y lo demas se devuelve
/// en el <see cref="MacFile"/> (resource fork incluido).
/// </summary>
public static async Task<MacFile> DecodeAsync(Stream input, Header header, Stream dataFork, CancellationToken ct)
{
await SkipAsync(input, HeaderSize + Pad(header.SecondaryHeader), ct);
await CopyExactAsync(input, dataFork, header.DataLength, ct);
await SkipAsync(input, Pad(header.DataLength) - header.DataLength, ct, allowShort: header.ResourceLength == 0);
var resource = new byte[header.ResourceLength];
await input.ReadExactlyAsync(resource, ct);
header.File.ResourceFork = resource;
return header.File;
}
/// <summary>Tamano total del MacBinary III que <see cref="EncodeAsync"/> escribiria.</summary>
public static long EncodedLength(long dataLength, int resourceLength) =>
HeaderSize + Pad(dataLength) + Pad(resourceLength);
/// <summary>Escribe MacBinary III (con CRC: tambien lo leen los decodificadores de MacBinary II).</summary>
public static async Task EncodeAsync(MacFile file, Stream dataFork, long dataLength, Stream output, CancellationToken ct)
{
await output.WriteAsync(BuildHeader(file, dataLength), ct);
await CopyExactAsync(dataFork, output, dataLength, ct);
await output.WriteAsync(new byte[Pad(dataLength) - dataLength], ct);
await output.WriteAsync(file.ResourceFork, ct);
await output.WriteAsync(new byte[Pad(file.ResourceFork.Length) - file.ResourceFork.Length], ct);
}
internal static byte[] BuildHeader(MacFile file, long dataLength)
{
var h = new byte[HeaderSize];
var name = Encodings.MacRoman.GetBytes(MacNames.Shorten(file.Name, 31));
h[1] = (byte)name.Length;
name.CopyTo(h, 2);
file.FinderInfo.AsSpan(0, 8).CopyTo(h.AsSpan(65));
h[73] = (byte)(file.FinderFlags >> 8);
file.FinderInfo.AsSpan(10, 4).CopyTo(h.AsSpan(75));
file.FinderInfo.AsSpan(14, 2).CopyTo(h.AsSpan(79));
BinaryPrimitives.WriteUInt32BigEndian(h.AsSpan(83), (uint)dataLength);
BinaryPrimitives.WriteUInt32BigEndian(h.AsSpan(87), (uint)file.ResourceFork.Length);
BinaryPrimitives.WriteUInt32BigEndian(h.AsSpan(91), MacFile.ToMacSeconds(file.Created ?? file.Modified));
BinaryPrimitives.WriteUInt32BigEndian(h.AsSpan(95), MacFile.ToMacSeconds(file.Modified));
h[101] = (byte)(file.FinderFlags & 0xFF);
"mBIN"u8.CopyTo(h.AsSpan(102));
h[106] = file.FinderInfo[24];
h[107] = file.FinderInfo[25];
h[122] = 130; // MacBinary III
h[123] = 129; // basta un lector de MacBinary II
BinaryPrimitives.WriteUInt16BigEndian(h.AsSpan(124), Crc16.Compute(h.AsSpan(0, 124)));
return h;
}
internal static long Pad(long length) => (length + 127) / 128 * 128;
private static async Task SkipAsync(Stream input, long count, CancellationToken ct, bool allowShort = false)
{
var buffer = new byte[Math.Min(65536, Math.Max(1, count))];
while (count > 0)
{
int n = await input.ReadAsync(buffer.AsMemory(0, (int)Math.Min(buffer.Length, count)), ct);
if (n == 0)
{
if (allowShort)
return;
throw new EndOfStreamException();
}
count -= n;
}
}
private static async Task CopyExactAsync(Stream input, Stream output, long count, CancellationToken ct)
{
var buffer = new byte[65536];
while (count > 0)
{
int n = await input.ReadAsync(buffer.AsMemory(0, (int)Math.Min(buffer.Length, count)), ct);
if (n == 0)
throw new EndOfStreamException();
await output.WriteAsync(buffer.AsMemory(0, n), ct);
count -= n;
}
}
}