Newer
Older
Sunfish / src / Sunfish.Core / Mac / AppleDouble.cs
@Ivan Dominguez Ivan Dominguez 21 hours ago 4 KB 3.0
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;
}