File: src\Aspire.Tray\Mac\MacTrayLog.cs
Web Access
Project: src\tests\Aspire.Tray.Tests\Aspire.Tray.Tests.csproj (Aspire.Tray.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using Microsoft.Win32.SafeHandles;
 
namespace Aspire.Tray;
 
[SupportedOSPlatform("macos")]
internal static partial class MacTrayLog
{
    private const int NoFollow = 0x100;
    private const int DirectoryOnly = 0x100000;
    private const int CloseOnExec = 0x1000000;
 
    public static SafeFileHandle Open(string path)
    {
        if (!Path.IsPathFullyQualified(path))
        {
            throw new ArgumentException("An absolute log path is required.", nameof(path));
        }
 
        // Walk from an open root, not a checked pathname. Each component is opened relative
        // to the previous descriptor with O_NOFOLLOW; renaming a parent cannot redirect us.
        // https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/open.2.html
        var components = Path.GetFullPath(path).Split('/', StringSplitOptions.RemoveEmptyEntries);
        if (components.Length == 0)
        {
            throw new ArgumentException("A log filename is required.", nameof(path));
        }
        var directory = OpenHandle(-2, "/", NoFollow | DirectoryOnly | CloseOnExec, 0);
        try
        {
            for (var index = 0; index < components.Length - 1; index++)
            {
                var next = OpenAt(directory, components[index], NoFollow | DirectoryOnly | CloseOnExec, 0);
                if (next < 0 && Marshal.GetLastPInvokeError() == 2)
                {
                    if (MakeDirectoryAt(directory, components[index], 0x1c0) != 0 && Marshal.GetLastPInvokeError() != 17)
                    {
                        throw Error("create the tray log directory");
                    }
                    next = OpenAt(directory, components[index], NoFollow | DirectoryOnly | CloseOnExec, 0);
                }
                if (next < 0)
                {
                    throw Error("open the tray log directory without following links");
                }
                directory.Dispose();
                directory = new SafeFileHandle(next, ownsHandle: true);
            }
 
            var parentStat = Stat(directory);
            if (parentStat.Owner != GetEffectiveUserId())
            {
                throw new IOException("The tray log directory must belong to the current user.");
            }
            if (ChangeMode(directory, 0x1c0) != 0)
            {
                throw Error("protect the tray log directory");
            }
 
            // O_WRONLY | O_APPEND | O_CREAT | O_NONBLOCK. Nonblocking avoids hanging on a
            // substituted FIFO before fstat rejects anything except a single-link regular file.
            var file = OpenHandle(directory.DangerousGetHandle().ToInt32(), components[^1],
                1 | 8 | 0x200 | 4 | NoFollow | CloseOnExec, 0x180);
            try
            {
                var stat = Stat(file);
                if ((stat.Mode & 0xf000) != 0x8000 || stat.LinkCount != 1 || stat.Owner != GetEffectiveUserId())
                {
                    throw new IOException("The tray log must be a regular, single-link file owned by the current user.");
                }
                if (ChangeMode(file, 0x180) != 0)
                {
                    throw Error("protect the tray log file");
                }
                return file;
            }
            catch
            {
                file.Dispose();
                throw;
            }
        }
        finally
        {
            directory.Dispose();
        }
    }
 
    private static SafeFileHandle OpenHandle(int directory, string name, int flags, ushort mode)
    {
        var descriptor = OpenAtRaw(directory, name, flags, mode, 0, 0, 0, 0, mode);
        if (descriptor < 0)
        {
            throw Error("open the tray log without following links");
        }
        return new SafeFileHandle(descriptor, ownsHandle: true);
    }
 
    private static FileStatus Stat(SafeFileHandle handle)
    {
        // Darwin x64's unsuffixed fstat has the legacy inode layout; arm64 uses stat64.
        var result = RuntimeInformation.ProcessArchitecture == Architecture.X64
            ? GetStatus64(handle, out var status) : GetStatus(handle, out status);
        if (result != 0)
        {
            throw Error("inspect the tray log descriptor");
        }
        return status;
    }
 
    private static IOException Error(string operation)
        => new($"Could not {operation}.", new Win32Exception(Marshal.GetLastPInvokeError()));
 
    private static int OpenAt(SafeFileHandle directory, string path, int flags, ushort mode)
        => OpenAtNative(directory, path, flags, mode, 0, 0, 0, 0, mode);
 
    // openat is variadic. Darwin arm64 reads mode from the stack, while x64 reads
    // argument four. Supply both locations; the unused register arguments are ignored.
    // https://developer.apple.com/documentation/xcode/writing-arm64-code-for-apple-platforms
    [LibraryImport("/usr/lib/libSystem.B.dylib", EntryPoint = "openat", SetLastError = true, StringMarshalling = StringMarshalling.Utf8)]
    private static partial int OpenAtNative(SafeFileHandle directory, string path, int flags, uint mode,
        nint unused1, nint unused2, nint unused3, nint unused4, uint stackMode);
 
    [LibraryImport("/usr/lib/libSystem.B.dylib", EntryPoint = "openat", SetLastError = true, StringMarshalling = StringMarshalling.Utf8)]
    private static partial int OpenAtRaw(int directory, string path, int flags, uint mode,
        nint unused1, nint unused2, nint unused3, nint unused4, uint stackMode);
 
    [LibraryImport("/usr/lib/libSystem.B.dylib", EntryPoint = "mkdirat", SetLastError = true, StringMarshalling = StringMarshalling.Utf8)]
    private static partial int MakeDirectoryAt(SafeFileHandle directory, string path, ushort mode);
 
    [LibraryImport("/usr/lib/libSystem.B.dylib", EntryPoint = "fchmod", SetLastError = true)]
    private static partial int ChangeMode(SafeFileHandle file, ushort mode);
 
    [LibraryImport("/usr/lib/libSystem.B.dylib", EntryPoint = "geteuid")]
    private static partial uint GetEffectiveUserId();
 
    [LibraryImport("/usr/lib/libSystem.B.dylib", EntryPoint = "fstat", SetLastError = true)]
    private static partial int GetStatus(SafeFileHandle file, out FileStatus status);
 
    [LibraryImport("/usr/lib/libSystem.B.dylib", EntryPoint = "fstat$INODE64", SetLastError = true)]
    private static partial int GetStatus64(SafeFileHandle file, out FileStatus status);
 
    // Darwin's stat64 is 144 bytes on both supported 64-bit architectures.
    [StructLayout(LayoutKind.Explicit, Size = 144)]
    private struct FileStatus
    {
        [FieldOffset(4)] public ushort Mode;
        [FieldOffset(6)] public ushort LinkCount;
        [FieldOffset(16)] public uint Owner;
    }
}