Files
duplicati/Duplicati/Library/Snapshots/MacOS/MacOSPhotosNative.cs
T

378 lines
14 KiB
C#

// Copyright (C) 2026, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
namespace Duplicati.Library.Snapshots.MacOS;
/// <summary>
/// Wrapper for native MacOS Photos library access
/// </summary>
[SupportedOSPlatform("macOS")]
internal static class MacOSPhotosNative
{
/// <summary>
/// The name of the native library
/// </summary>
private const string LibraryName = "DuplicatiPhotos";
/// <summary>
/// Lists all assets in the Photos library
/// </summary>
/// <returns>The list of assets</returns>
public static IReadOnlyList<NativeAsset> ListAssets()
{
var result = NativeMethods.DuplicatiPhotosEnumerateAssets(out var assetsPtr, out var count, out var errorPtr);
try
{
if (result != 0)
throw new MacOSPhotosException(ConsumeErrorMessage(ref errorPtr) ?? "Failed to enumerate Photos assets.");
if (assetsPtr == IntPtr.Zero || count == UIntPtr.Zero)
return Array.Empty<NativeAsset>();
var assetCount = checked((int)count.ToUInt64());
var structSize = Marshal.SizeOf<NativeAssetNative>();
var assets = new List<NativeAsset>(assetCount);
for (var index = 0; index < assetCount; index++)
{
var entryPtr = assetsPtr + (index * structSize);
var native = Marshal.PtrToStructure<NativeAssetNative>(entryPtr);
var identifier = Marshal.PtrToStringUTF8(native.Identifier) ?? string.Empty;
var fileName = Marshal.PtrToStringUTF8(native.FileName) ?? string.Empty;
var uti = Marshal.PtrToStringUTF8(native.UniformTypeIdentifier);
assets.Add(new NativeAsset(
identifier,
fileName,
uti,
(MacOSPhotoMediaType)native.MediaType,
native.Size,
native.PixelWidth,
native.PixelHeight,
NormalizeOptional(native.CreationSeconds),
NormalizeOptional(native.ModificationSeconds)));
}
return assets;
}
finally
{
if (assetsPtr != IntPtr.Zero)
NativeMethods.DuplicatiPhotosFreeAssets(assetsPtr, count);
if (errorPtr != IntPtr.Zero)
NativeMethods.DuplicatiPhotosFreeString(errorPtr);
}
}
/// <summary>
/// Opens a read stream for the specified asset identifier
/// </summary>
/// <param name="identifier">The unique identifier of the asset</param>
/// <param name="size">The size of the asset, or null to determine size automatically</param>
/// <returns>The read stream for the asset</returns>
public static Stream OpenReadStream(string identifier, long? size = null)
{
if (string.IsNullOrWhiteSpace(identifier))
throw new ArgumentException("Identifier is required", nameof(identifier));
var result = NativeMethods.DuplicatiPhotosOpenAsset(identifier, out var handle, out var errorPtr);
if (result != 0 || handle.IsInvalid)
{
var message = ConsumeErrorMessage(ref errorPtr) ?? "Failed to open Photos asset.";
handle.Dispose();
throw new MacOSPhotosException(message);
}
// If size is not provided, try to get it from the handle
var assetSize = size ?? GetAssetSize(handle);
return new MacOSPhotosNativeStream(handle, assetSize);
}
/// <summary>
/// Gets the size of the asset from the handle
/// </summary>
/// <param name="handle">The asset handle</param>
/// <returns>The size of the asset</returns>
internal static long GetAssetSize(SafeAssetHandle handle)
{
if (handle.IsInvalid)
throw new ObjectDisposedException(nameof(SafeAssetHandle));
var result = NativeMethods.DuplicatiPhotosGetAssetSize(handle, out var size, out var errorPtr);
if (result != 0)
{
var message = ConsumeErrorMessage(ref errorPtr) ?? "Failed to get Photos asset size.";
throw new MacOSPhotosException(message);
}
if (errorPtr != IntPtr.Zero)
{
NativeMethods.DuplicatiPhotosFreeString(errorPtr);
}
return size;
}
/// <summary>
/// Reads data from the asset into the provided buffer
/// </summary>
/// <param name="handle">The asset handle</param>
/// <param name="buffer">The buffer to read data into</param>
/// <returns>The number of bytes read</returns>
internal static int ReadAsset(SafeAssetHandle handle, Span<byte> buffer)
{
if (handle.IsInvalid)
throw new ObjectDisposedException(nameof(SafeAssetHandle));
if (buffer.Length == 0)
return 0;
var errorPtr = IntPtr.Zero;
int bytesRead;
// TODO: Optimize to read directly into the provided buffer
var tempbuffer = new byte[buffer.Length];
var result = NativeMethods.DuplicatiPhotosReadAsset(handle, tempbuffer, (nuint)tempbuffer.Length, out errorPtr);
if (result < 0)
{
var message = ConsumeErrorMessage(ref errorPtr) ?? "Failed to read Photos asset data.";
throw new MacOSPhotosException(message);
}
tempbuffer.AsSpan(0, (int)result).CopyTo(buffer);
bytesRead = checked((int)result);
if (errorPtr != IntPtr.Zero)
{
NativeMethods.DuplicatiPhotosFreeString(errorPtr);
errorPtr = IntPtr.Zero;
}
return bytesRead;
}
/// <summary>
/// Normalizes an optional double value, returning null if it is NaN
/// </summary>
/// <param name="value">The double value</param>
/// <returns>>The normalized value or null</returns>
private static double? NormalizeOptional(double value)
=> double.IsNaN(value) ? null : value;
/// <summary>
/// Consumes an error message pointer and frees the native memory
/// </summary>
/// <param name="pointer">The error message pointer</param>
/// <returns>The error message string or null</returns>
private static string? ConsumeErrorMessage(ref IntPtr pointer)
{
if (pointer == IntPtr.Zero)
return null;
try
{
return Marshal.PtrToStringUTF8(pointer);
}
finally
{
NativeMethods.DuplicatiPhotosFreeString(pointer);
pointer = IntPtr.Zero;
}
}
/// <summary>
/// Represents a native asset in the Photos library
/// </summary>
/// <param name="Identifier">The unique identifier of the asset</param>
/// <param name="FileName">The original file name</param>
/// <param name="UniformTypeIdentifier">The Uniform Type Identifier of the asset</param>
/// <param name="MediaType">The media type of the asset</param>
/// <param name="Size">The size of the asset in bytes</param>
/// <param name="PixelWidth">The pixel width of the asset</param>
/// <param name="PixelHeight">>The pixel height of the asset</param>
/// <param name="CreationSeconds">The creation time in seconds since Unix epoch</param>
/// <param name="ModificationSeconds">>The modification time in seconds since Unix epoch</param>
public sealed record NativeAsset(
string Identifier,
string FileName,
string? UniformTypeIdentifier,
MacOSPhotoMediaType MediaType,
long Size,
int PixelWidth,
int PixelHeight,
double? CreationSeconds,
double? ModificationSeconds);
/// <summary>
/// Native representation of an asset
/// </summary>
[StructLayout(LayoutKind.Sequential)]
private struct NativeAssetNative
{
/// <summary>
/// The unique identifier of the asset
/// </summary>
internal IntPtr Identifier;
/// <summary>
/// The original file name
/// </summary>
internal IntPtr FileName;
/// <summary>
/// The Uniform Type Identifier of the asset
/// </summary>
internal IntPtr UniformTypeIdentifier;
/// <summary>
/// The size of the asset in bytes
/// </summary>
internal long Size;
/// <summary>
/// The media type of the asset
/// </summary>
internal int MediaType;
/// <summary>
/// The pixel width of the asset
/// </summary>
internal int PixelWidth;
/// <summary>
/// The pixel height of the asset
/// </summary>
internal int PixelHeight;
/// <summary>
/// The creation time in seconds since Unix epoch
/// </summary>
internal double CreationSeconds;
/// <summary>
/// The modification time in seconds since Unix epoch
/// </summary>
internal double ModificationSeconds;
}
/// <summary>
/// Safe handle for a Photos asset
/// </summary>
internal sealed class SafeAssetHandle : SafeHandle
{
/// <summary>
/// Initializes a new instance of the <see cref="SafeAssetHandle"/> class
/// </summary>
private SafeAssetHandle()
: base(IntPtr.Zero, true)
{
}
/// <summary>
/// Indicates if the handle is invalid
/// </summary>
public override bool IsInvalid => handle == IntPtr.Zero;
/// <summary>
/// Releases the handle
/// </summary>
/// <returns>True if the handle was released successfully; otherwise, false</returns>
protected override bool ReleaseHandle()
{
if (!IsInvalid)
NativeMethods.DuplicatiPhotosCloseAsset(handle);
return true;
}
}
/// <summary>
/// Native method imports
/// </summary>
private static class NativeMethods
{
/// <summary>
/// Enumerates the assets in the Photos library
/// </summary>
/// <param name="assets">The pointer to the array of assets</param>
/// <param name="count">The number of assets</param>
/// <param name="errorMessage">The error message, if any</param>
/// <returns>The result code</returns>
[DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)]
internal static extern int DuplicatiPhotosEnumerateAssets(out IntPtr assets, out UIntPtr count, out IntPtr errorMessage);
/// <summary>
/// Frees the assets array
/// </summary>
/// <param name="assets">The pointer to the array of assets</param>
/// <param name="count">The number of assets</param>
[DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)]
internal static extern void DuplicatiPhotosFreeAssets(IntPtr assets, UIntPtr count);
/// <summary>
/// Frees a string allocated by the native library
/// </summary>
/// <param name="value">The string pointer</param>
[DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)]
internal static extern void DuplicatiPhotosFreeString(IntPtr value);
/// <summary>
/// Opens an asset for reading
/// </summary>
/// <param name="identifier">The unique identifier of the asset</param>
/// <param name="handle">The output asset handle</param>
/// <param name="errorMessage">The error message, if any</param>
/// <returns>The result code</returns>
[DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)]
internal static extern int DuplicatiPhotosOpenAsset([MarshalAs(UnmanagedType.LPUTF8Str)] string identifier, out SafeAssetHandle handle, out IntPtr errorMessage);
/// <summary>
/// Reads data from the asset into the provided buffer
/// </summary>
/// <param name="handle">The asset handle</param>
/// <param name="buffer">The buffer to read data into</param>
/// <param name="length">The length of the buffer</param>
/// <param name="errorMessage"> The error message, if any</param>
/// <returns>The number of bytes read</returns>
[DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)]
internal static extern nint DuplicatiPhotosReadAsset(SafeAssetHandle handle, byte[] buffer, nuint length, out IntPtr errorMessage);
/// <summary>
/// Closes the asset handle
/// </summary>
/// <param name="handle">The asset handle</param>
[DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)]
internal static extern void DuplicatiPhotosCloseAsset(IntPtr handle);
/// <summary>
/// Gets the size of the asset
/// </summary>
/// <param name="handle">The asset handle</param>
/// <param name="size">The size of the asset</param>
/// <param name="errorMessage">The error message, if any</param>
/// <returns>The result code</returns>
[DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)]
internal static extern int DuplicatiPhotosGetAssetSize(SafeAssetHandle handle, out long size, out IntPtr errorMessage);
}
}