From d50205cc9f2a983e029e0b5c626f7510c1b4e7ec Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sun, 14 Jun 2026 14:35:26 +0200 Subject: [PATCH 1/5] Follow native interoperability best practices https://learn.microsoft.com/en-us/dotnet/standard/native-interop/best-practices --- .../Encoder/ApplePlatformHelper.cs | 40 ++++++++----------- .../MediaBrowser.MediaEncoding.csproj | 1 + .../IO/ManagedFileSystemTests.cs | 6 +-- ...llyfin.Server.Implementations.Tests.csproj | 1 + 4 files changed, 22 insertions(+), 26 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs b/MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs index a8ff58b091..dba47bf615 100644 --- a/MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs +++ b/MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs @@ -1,9 +1,11 @@ #pragma warning disable CA1031 using System; +using System.Buffers; using System.Linq; using System.Runtime.InteropServices; using System.Runtime.Versioning; +using System.Text; using Microsoft.Extensions.Logging; namespace MediaBrowser.MediaEncoding.Encoder; @@ -12,43 +14,38 @@ namespace MediaBrowser.MediaEncoding.Encoder; /// Helper class for Apple platform specific operations. /// [SupportedOSPlatform("macos")] -public static class ApplePlatformHelper +public static partial class ApplePlatformHelper { private static readonly string[] _av1DecodeBlacklistedCpuClass = ["M1", "M2"]; - private static string GetSysctlValue(ReadOnlySpan name) + private static string GetSysctlValue(string name) { IntPtr length = IntPtr.Zero; // Get length of the value - int osStatus = SysctlByName(name, IntPtr.Zero, ref length, IntPtr.Zero, 0); - - if (osStatus != 0) + int osStatus = sysctlbyname(name, Span.Empty, ref length, IntPtr.Zero, 0); + if (osStatus != 0 || length == 0) { - throw new NotSupportedException($"Failed to get sysctl value for {System.Text.Encoding.UTF8.GetString(name)} with error {osStatus}"); + throw new NotSupportedException($"Failed to get sysctl value for {name} with error {osStatus}"); } - IntPtr buffer = Marshal.AllocHGlobal(length.ToInt32()); + byte[] buffer = ArrayPool.Shared.Rent((int)length); try { - osStatus = SysctlByName(name, buffer, ref length, IntPtr.Zero, 0); + osStatus = sysctlbyname(name, buffer.AsSpan().Slice(0, (int)length), ref length, IntPtr.Zero, 0); if (osStatus != 0) { - throw new NotSupportedException($"Failed to get sysctl value for {System.Text.Encoding.UTF8.GetString(name)} with error {osStatus}"); + throw new NotSupportedException($"Failed to get sysctl value for {name} with error {osStatus}"); } - return Marshal.PtrToStringAnsi(buffer) ?? string.Empty; + ReadOnlySpan data = buffer.AsSpan().Slice(0, (int)length); + return Encoding.UTF8.GetString(data); } finally { - Marshal.FreeHGlobal(buffer); + ArrayPool.Shared.Return(buffer); } } - private static int SysctlByName(ReadOnlySpan name, IntPtr oldp, ref IntPtr oldlenp, IntPtr newp, uint newlen) - { - return NativeMethods.SysctlByName(name.ToArray(), oldp, ref oldlenp, newp, newlen); - } - /// /// Check if the current system has hardware acceleration for AV1 decoding. /// @@ -63,7 +60,7 @@ public static class ApplePlatformHelper try { - string cpuBrandString = GetSysctlValue("machdep.cpu.brand_string"u8); + string cpuBrandString = GetSysctlValue("machdep.cpu.brand_string"); return !_av1DecodeBlacklistedCpuClass.Any(blacklistedCpuClass => cpuBrandString.Contains(blacklistedCpuClass, StringComparison.OrdinalIgnoreCase)); } catch (NotSupportedException e) @@ -78,10 +75,7 @@ public static class ApplePlatformHelper return false; } - private static class NativeMethods - { - [DllImport("libc", EntryPoint = "sysctlbyname", SetLastError = true)] - [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] - internal static extern int SysctlByName(byte[] name, IntPtr oldp, ref IntPtr oldlenp, IntPtr newp, uint newlen); - } + [LibraryImport("libc", EntryPoint = "sysctlbyname", SetLastError = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] + internal static partial int sysctlbyname([MarshalAs(UnmanagedType.LPStr)] string name, Span oldp, ref IntPtr oldlenp, IntPtr newp, uint newlen); } diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index fc11047a7f..288cd3e189 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -9,6 +9,7 @@ net10.0 false true + true diff --git a/tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs b/tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs index c06279af2d..6cadfacce8 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs @@ -10,7 +10,7 @@ using Xunit; namespace Jellyfin.Server.Implementations.Tests.IO; -public class ManagedFileSystemTests +public partial class ManagedFileSystemTests { private readonly IFixture _fixture; private readonly ManagedFileSystem _sut; @@ -117,7 +117,7 @@ public class ManagedFileSystemTests } [SuppressMessage("Naming Rules", "SA1300:ElementMustBeginWithUpperCaseLetter", Justification = "Have to")] - [DllImport("libc", SetLastError = true, CharSet = CharSet.Ansi)] + [LibraryImport("libc", SetLastError = true)] [DefaultDllImportSearchPaths(DllImportSearchPath.UserDirectories)] - private static extern int symlink(string target, string linkpath); + private static partial int symlink([MarshalAs(UnmanagedType.LPStr)] string target, [MarshalAs(UnmanagedType.LPStr)] string linkpath); } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj index 958ffb8b6e..29de52a2ba 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj +++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj @@ -4,6 +4,7 @@ {2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE} Exe + true From a9a02719abd0faefbd90550f6ba576d2f63db436 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Mon, 15 Jun 2026 18:12:22 +0200 Subject: [PATCH 2/5] Fix type of length arguments --- MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs b/MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs index dba47bf615..14a628e226 100644 --- a/MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs +++ b/MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs @@ -20,7 +20,7 @@ public static partial class ApplePlatformHelper private static string GetSysctlValue(string name) { - IntPtr length = IntPtr.Zero; + nuint length = 0; // Get length of the value int osStatus = sysctlbyname(name, Span.Empty, ref length, IntPtr.Zero, 0); if (osStatus != 0 || length == 0) @@ -31,13 +31,13 @@ public static partial class ApplePlatformHelper byte[] buffer = ArrayPool.Shared.Rent((int)length); try { - osStatus = sysctlbyname(name, buffer.AsSpan().Slice(0, (int)length), ref length, IntPtr.Zero, 0); + osStatus = sysctlbyname(name, buffer.AsSpan()[..(int)length], ref length, IntPtr.Zero, 0); if (osStatus != 0) { throw new NotSupportedException($"Failed to get sysctl value for {name} with error {osStatus}"); } - ReadOnlySpan data = buffer.AsSpan().Slice(0, (int)length); + ReadOnlySpan data = buffer.AsSpan()[..(int)length]; return Encoding.UTF8.GetString(data); } finally @@ -77,5 +77,5 @@ public static partial class ApplePlatformHelper [LibraryImport("libc", EntryPoint = "sysctlbyname", SetLastError = true)] [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] - internal static partial int sysctlbyname([MarshalAs(UnmanagedType.LPStr)] string name, Span oldp, ref IntPtr oldlenp, IntPtr newp, uint newlen); + internal static partial int sysctlbyname([MarshalAs(UnmanagedType.LPStr)] string name, Span oldp, ref nuint oldlenp, IntPtr newp, nuint newlen); } From 0022508889adb8b60bde8bc5e69640d3ff8dd346 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Mon, 15 Jun 2026 21:20:06 +0200 Subject: [PATCH 3/5] Add regression test --- .../Encoder/ApplePlatformHelper.cs | 2 +- .../Encoder/ApplePlatformHelperTests.cs | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 tests/Jellyfin.MediaEncoding.Tests/Encoder/ApplePlatformHelperTests.cs diff --git a/MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs b/MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs index 14a628e226..8cae6a238d 100644 --- a/MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs +++ b/MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs @@ -18,7 +18,7 @@ public static partial class ApplePlatformHelper { private static readonly string[] _av1DecodeBlacklistedCpuClass = ["M1", "M2"]; - private static string GetSysctlValue(string name) + internal static string GetSysctlValue(string name) { nuint length = 0; // Get length of the value diff --git a/tests/Jellyfin.MediaEncoding.Tests/Encoder/ApplePlatformHelperTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Encoder/ApplePlatformHelperTests.cs new file mode 100644 index 0000000000..586a382485 --- /dev/null +++ b/tests/Jellyfin.MediaEncoding.Tests/Encoder/ApplePlatformHelperTests.cs @@ -0,0 +1,18 @@ +using System; +using System.Runtime.Versioning; +using MediaBrowser.MediaEncoding.Encoder; +using Xunit; + +namespace Jellyfin.MediaEncoding.Tests; + +[SupportedOSPlatform("macos")] +public class ApplePlatformHelperTests +{ + [Fact] + public void GetSysctlValue_CpuBrand_NotEmpty() + { + Assert.SkipUnless(OperatingSystem.IsMacOS(), "macOS-only test"); + + Assert.NotEmpty(ApplePlatformHelper.GetSysctlValue("machdep.cpu.brand_string")); + } +} From 4c228eaf63c0b60e40a5a4b82aa66af336750f08 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Tue, 16 Jun 2026 17:45:22 +0200 Subject: [PATCH 4/5] Make sure we don't include the null terminator --- .../Encoder/ApplePlatformHelperTests.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/Jellyfin.MediaEncoding.Tests/Encoder/ApplePlatformHelperTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Encoder/ApplePlatformHelperTests.cs index 586a382485..9847acbb0a 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Encoder/ApplePlatformHelperTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Encoder/ApplePlatformHelperTests.cs @@ -13,6 +13,10 @@ public class ApplePlatformHelperTests { Assert.SkipUnless(OperatingSystem.IsMacOS(), "macOS-only test"); - Assert.NotEmpty(ApplePlatformHelper.GetSysctlValue("machdep.cpu.brand_string")); + var value = ApplePlatformHelper.GetSysctlValue("machdep.cpu.brand_string"); + Assert.NotEmpty(value); + + // Make sure we don't include the null terminator + Assert.DoesNotContain("\0", value, StringComparison.Ordinal); } } From e86b502cbc9d48c876cc125f3c316171c1113926 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Tue, 16 Jun 2026 17:54:23 +0200 Subject: [PATCH 5/5] Strip null-terminator --- MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs b/MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs index 8cae6a238d..baea0df8cc 100644 --- a/MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs +++ b/MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs @@ -37,7 +37,12 @@ public static partial class ApplePlatformHelper throw new NotSupportedException($"Failed to get sysctl value for {name} with error {osStatus}"); } - ReadOnlySpan data = buffer.AsSpan()[..(int)length]; + if (length < 1) + { + return string.Empty; + } + + ReadOnlySpan data = buffer.AsSpan()[..(int)(length - 1)]; return Encoding.UTF8.GetString(data); } finally