using System.Buffers.Binary;
namespace Sunfish.Mac;
/// <summary>
/// AppleDouble version 2: el fichero "._nombre" que acompana a "nombre" y guarda lo que no cabe en
/// un sistema de ficheros sin forks. Es el mismo formato que usa macOS en FAT, SMB, etc.
/// Se escriben las entradas 9 (Finder Info, 32 bytes), 8 (fechas) y 2 (resource fork).
/// </summary>
public static class AppleDouble
{
public const string Prefix = "._";
private const uint Magic = 0x00051607;
private const uint Version = 0x00020000;
public static string CompanionOf(string physical) =>
Path.Combine(Path.GetDirectoryName(physical)!, Prefix + Path.GetFileName(physical));
public static byte[] Write(MacFile file)
{
var entries = new List<(uint Id, byte[] Data)>
{
(9, file.FinderInfo),
(8, Dates(file)),
};
if (file.ResourceFork.Length > 0)
entries.Add((2, file.ResourceFork)); // el resource fork al final, como hace macOS
int headerLength = 26 + 12 * entries.Count;
var result = new byte[headerLength + entries.Sum(e => e.Data.Length)];
var s = result.AsSpan();
BinaryPrimitives.WriteUInt32BigEndian(s, Magic);
BinaryPrimitives.WriteUInt32BigEndian(s[4..], Version);
"Mac OS X "u8.CopyTo(s[8..]); // filler de 16 bytes que escribe macOS
BinaryPrimitives.WriteUInt16BigEndian(s[24..], (ushort)entries.Count);
int offset = headerLength;
for (int i = 0; i < entries.Count; i++)
{
var e = s[(26 + 12 * i)..];
BinaryPrimitives.WriteUInt32BigEndian(e, entries[i].Id);
BinaryPrimitives.WriteUInt32BigEndian(e[4..], (uint)offset);
BinaryPrimitives.WriteUInt32BigEndian(e[8..], (uint)entries[i].Data.Length);
entries[i].Data.CopyTo(s[offset..]);
offset += entries[i].Data.Length;
}
return result;
}
/// <summary>Lee un AppleDouble; null si no lo es.</summary>
public static MacFile? Read(ReadOnlySpan<byte> data, string name = "")
{
if (data.Length < 26 || BinaryPrimitives.ReadUInt32BigEndian(data) != Magic)
return null;
int count = BinaryPrimitives.ReadUInt16BigEndian(data[24..]);
if (26 + 12 * count > data.Length)
return null;
var file = new MacFile { Name = name };
for (int i = 0; i < count; i++)
{
var e = data[(26 + 12 * i)..];
uint id = BinaryPrimitives.ReadUInt32BigEndian(e);
int offset = (int)BinaryPrimitives.ReadUInt32BigEndian(e[4..]);
int length = (int)BinaryPrimitives.ReadUInt32BigEndian(e[8..]);
if (offset < 0 || length < 0 || offset + length > data.Length)
return null;
var payload = data.Slice(offset, length);
switch (id)
{
case 2:
file.ResourceFork = payload.ToArray();
break;
case 9:
payload[..Math.Min(32, payload.Length)].CopyTo(file.FinderInfo);
break;
case 8 when length >= 8:
// Fechas de AppleDouble: segundos con signo desde el 1 de enero de 2000 (UTC).
file.Created = FromDoubleDate(BinaryPrimitives.ReadInt32BigEndian(payload));
file.Modified = FromDoubleDate(BinaryPrimitives.ReadInt32BigEndian(payload[4..]));
break;
}
}
return file;
}
public static MacFile? ReadCompanion(string physical)
{
string companion = CompanionOf(physical);
if (!File.Exists(companion))
return null;
try
{
return Read(File.ReadAllBytes(companion), Path.GetFileName(physical));
}
catch (IOException)
{
return null;
}
}
private static readonly DateTime DoubleEpoch = new(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private static DateTime? FromDoubleDate(int seconds) =>
seconds == int.MinValue ? null : DoubleEpoch.AddSeconds(seconds).ToLocalTime();
private static byte[] Dates(MacFile file)
{
var data = new byte[16];
BinaryPrimitives.WriteInt32BigEndian(data, ToDoubleDate(file.Created));
BinaryPrimitives.WriteInt32BigEndian(data.AsSpan(4), ToDoubleDate(file.Modified));
BinaryPrimitives.WriteInt32BigEndian(data.AsSpan(8), int.MinValue); // backup: nunca
BinaryPrimitives.WriteInt32BigEndian(data.AsSpan(12), ToDoubleDate(file.Modified)); // acceso
return data;
}
private static int ToDoubleDate(DateTime? time) =>
time is { } t ? (int)Math.Clamp((t.ToUniversalTime() - DoubleEpoch).TotalSeconds, int.MinValue + 1, int.MaxValue) : int.MinValue;
}