From 8c39e877ff340fd2da1261cc6bea3d7bad9f8566 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Wed, 16 Sep 2026 14:00:53 +0200 Subject: [PATCH 1/4] Support devicectl JSON version 5 Parse the new properties dictionary with compatibility fallbacks for deprecated fields, and cover the new JSON shape. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Xamarin.MacDev/DeviceCtlOutputParser.cs | 87 ++++++++++++++++--------- tests/DeviceCtlOutputParserTests.cs | 63 ++++++++++++++++++ 2 files changed, 119 insertions(+), 31 deletions(-) diff --git a/Xamarin.MacDev/DeviceCtlOutputParser.cs b/Xamarin.MacDev/DeviceCtlOutputParser.cs index 19e1076..d91b871 100644 --- a/Xamarin.MacDev/DeviceCtlOutputParser.cs +++ b/Xamarin.MacDev/DeviceCtlOutputParser.cs @@ -47,37 +47,32 @@ public static List ParseDevices (string? json, ICustomLogger Identifier = GetString (device, "identifier"), }; - // deviceProperties - if (device.TryGetProperty ("deviceProperties", out var deviceProps)) { - info.Name = GetString (deviceProps, "name"); - info.BuildVersion = GetString (deviceProps, "osBuildUpdate"); - info.OSVersion = GetString (deviceProps, "osVersionNumber"); - } - - // hardwareProperties - if (device.TryGetProperty ("hardwareProperties", out var hwProps)) { - info.Udid = GetString (hwProps, "udid"); - info.DeviceClass = GetString (hwProps, "deviceType"); - info.HardwareModel = GetString (hwProps, "hardwareModel"); - info.Platform = GetString (hwProps, "platform"); - info.ProductType = GetString (hwProps, "productType"); - info.SerialNumber = GetString (hwProps, "serialNumber"); - - if (hwProps.TryGetProperty ("ecid", out var ecidElement)) { - if (ecidElement.TryGetUInt64 (out var ecid)) - info.UniqueChipID = ecid; - } - - // cpuType.name - if (hwProps.TryGetProperty ("cpuType", out var cpuType)) - info.CpuArchitecture = GetString (cpuType, "name"); - } - - // connectionProperties - if (device.TryGetProperty ("connectionProperties", out var connProps)) { - info.TransportType = GetString (connProps, "transportType"); - info.PairingState = GetString (connProps, "pairingState"); - } + var properties = GetObject (device, "properties"); + var newDeviceProperties = GetObject (properties, "device"); + var stateProperties = GetObject (properties, "state"); + var newHardwareProperties = GetObject (properties, "hardware"); + var newConnectionProperties = GetObject (properties, "connection"); + var deviceProperties = GetObject (device, "deviceProperties"); + var hardwareProperties = GetObject (device, "hardwareProperties"); + var connectionProperties = GetObject (device, "connectionProperties"); + + info.Name = GetString ("name", newDeviceProperties, stateProperties, properties, deviceProperties); + info.BuildVersion = GetString ("osBuildUpdate", stateProperties, newDeviceProperties, properties, deviceProperties); + info.OSVersion = GetString ("osVersionNumber", stateProperties, newDeviceProperties, properties, deviceProperties); + info.Udid = GetString ("udid", newHardwareProperties, properties, hardwareProperties); + info.DeviceClass = GetString ("deviceType", newHardwareProperties, properties, hardwareProperties); + info.HardwareModel = GetString ("hardwareModel", newHardwareProperties, properties, hardwareProperties); + info.Platform = GetString ("platform", newHardwareProperties, properties, hardwareProperties); + info.ProductType = GetString ("productType", newHardwareProperties, properties, hardwareProperties); + info.SerialNumber = GetString ("serialNumber", newHardwareProperties, properties, hardwareProperties); + info.UniqueChipID = GetUInt64 ("ecid", newHardwareProperties, properties, hardwareProperties); + + var cpuType = GetObject (newHardwareProperties, "cpuType") ?? GetObject (properties, "cpuType") ?? GetObject (hardwareProperties, "cpuType"); + if (cpuType.HasValue) + info.CpuArchitecture = GetString (cpuType.Value, "name"); + + info.TransportType = GetString ("transportType", newConnectionProperties, stateProperties, properties, connectionProperties); + info.PairingState = GetString ("pairingState", newConnectionProperties, stateProperties, properties, connectionProperties); // Fallback: use identifier as UDID if hardware UDID is missing if (string.IsNullOrEmpty (info.Udid)) @@ -106,4 +101,34 @@ static string GetString (JsonElement element, string property) } return ""; } + + static JsonElement? GetObject (JsonElement? element, string property) + { + if (element.HasValue && element.Value.TryGetProperty (property, out var value) && value.ValueKind == JsonValueKind.Object) + return value; + return null; + } + + static string GetString (string property, params JsonElement? [] elements) + { + foreach (var element in elements) { + if (!element.HasValue) + continue; + var value = GetString (element.Value, property); + if (!string.IsNullOrEmpty (value)) + return value; + } + return ""; + } + + static ulong? GetUInt64 (string property, params JsonElement? [] elements) + { + foreach (var element in elements) { + if (!element.HasValue || !element.Value.TryGetProperty (property, out var value)) + continue; + if (value.TryGetUInt64 (out var result)) + return result; + } + return null; + } } diff --git a/tests/DeviceCtlOutputParserTests.cs b/tests/DeviceCtlOutputParserTests.cs index ee84b80..274b51e 100644 --- a/tests/DeviceCtlOutputParserTests.cs +++ b/tests/DeviceCtlOutputParserTests.cs @@ -61,6 +61,69 @@ public void ParseDevices_ValidJson_ParsesAllFields () Assert.That (device.PairingState, Is.EqualTo ("paired")); } + [Test] + public void ParseDevices_JsonVersion5_ParsesProperties () + { + var json = @"{ + ""info"": { + ""jsonVersion"": 5 + }, + ""result"": { + ""devices"": [ + { + ""_deprecationNotice"": { + ""deprecatedFields"": [ ""hardwareProperties"", ""deviceProperties"", ""connectionProperties"" ], + ""replacement"": ""properties"" + }, + ""identifier"": ""33333333-AAAA-BBBB-CCCC-DDDDDDDDDDDD"", + ""properties"": { + ""connection"": { + ""transportType"": ""localNetwork"" + }, + ""device"": { + ""name"": ""Rolf's iPhone 15"" + }, + ""hardware"": { + ""cpuType"": { ""name"": ""arm64e"" }, + ""deviceType"": ""iPhone"", + ""ecid"": 12345678, + ""hardwareModel"": ""D83AP"", + ""platform"": ""iOS"", + ""productType"": ""iPhone16,1"", + ""serialNumber"": ""SERIAL_1"", + ""udid"": ""00008003-012301230123ABCD"" + }, + ""state"": { + ""osBuildUpdate"": ""23B85"", + ""osVersionNumber"": ""18.1"", + ""pairingState"": ""paired"" + } + } + } + ] + } + }"; + + var result = DeviceCtlOutputParser.ParseDevices (json); + Assert.That (result.Count, Is.EqualTo (1)); + + var device = result [0]; + Assert.That (device.Name, Is.EqualTo ("Rolf's iPhone 15")); + Assert.That (device.Udid, Is.EqualTo ("00008003-012301230123ABCD")); + Assert.That (device.Identifier, Is.EqualTo ("33333333-AAAA-BBBB-CCCC-DDDDDDDDDDDD")); + Assert.That (device.BuildVersion, Is.EqualTo ("23B85")); + Assert.That (device.OSVersion, Is.EqualTo ("18.1")); + Assert.That (device.DeviceClass, Is.EqualTo ("iPhone")); + Assert.That (device.HardwareModel, Is.EqualTo ("D83AP")); + Assert.That (device.Platform, Is.EqualTo ("iOS")); + Assert.That (device.ProductType, Is.EqualTo ("iPhone16,1")); + Assert.That (device.SerialNumber, Is.EqualTo ("SERIAL_1")); + Assert.That (device.UniqueChipID, Is.EqualTo ((ulong) 12345678)); + Assert.That (device.CpuArchitecture, Is.EqualTo ("arm64e")); + Assert.That (device.TransportType, Is.EqualTo ("localNetwork")); + Assert.That (device.PairingState, Is.EqualTo ("paired")); + } + [Test] public void ParseDevices_MultipleDevices_ParsesAll () { From 53e27b107ec871cf4422b9a99f29652ffb88199f Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Wed, 16 Sep 2026 16:57:24 +0200 Subject: [PATCH 2/4] Fix devicectl version 5 nested property parsing Read build and OS version values from the nested software objects emitted by Xcode 27 while retaining the legacy compatibility fallbacks. Resolve CPU architecture by the first non-empty name so numeric-only v5 cpuType objects do not hide the deprecated named value. Update the v5 fixture to reflect the observed payload and exercise mixed-format fallback behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Xamarin.MacDev/DeviceCtlOutputParser.cs | 19 ++++++++++---- tests/DeviceCtlOutputParserTests.cs | 33 +++++++++++++++++++------ 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/Xamarin.MacDev/DeviceCtlOutputParser.cs b/Xamarin.MacDev/DeviceCtlOutputParser.cs index d91b871..6344dfe 100644 --- a/Xamarin.MacDev/DeviceCtlOutputParser.cs +++ b/Xamarin.MacDev/DeviceCtlOutputParser.cs @@ -50,6 +50,10 @@ public static List ParseDevices (string? json, ICustomLogger var properties = GetObject (device, "properties"); var newDeviceProperties = GetObject (properties, "device"); var stateProperties = GetObject (properties, "state"); + var softwareProperties = GetObject (properties, "software"); + var osBuildVersions = GetObject (softwareProperties, "osBuildVersions"); + var buildVersion = GetObject (osBuildVersions, "buildVersion"); + var osVersionNumber = GetObject (softwareProperties, "osVersionNumber"); var newHardwareProperties = GetObject (properties, "hardware"); var newConnectionProperties = GetObject (properties, "connection"); var deviceProperties = GetObject (device, "deviceProperties"); @@ -57,8 +61,12 @@ public static List ParseDevices (string? json, ICustomLogger var connectionProperties = GetObject (device, "connectionProperties"); info.Name = GetString ("name", newDeviceProperties, stateProperties, properties, deviceProperties); - info.BuildVersion = GetString ("osBuildUpdate", stateProperties, newDeviceProperties, properties, deviceProperties); - info.OSVersion = GetString ("osVersionNumber", stateProperties, newDeviceProperties, properties, deviceProperties); + info.BuildVersion = GetString ("name", buildVersion); + if (string.IsNullOrEmpty (info.BuildVersion)) + info.BuildVersion = GetString ("osBuildUpdate", stateProperties, newDeviceProperties, properties, deviceProperties); + info.OSVersion = GetString ("stringValue", osVersionNumber); + if (string.IsNullOrEmpty (info.OSVersion)) + info.OSVersion = GetString ("osVersionNumber", stateProperties, newDeviceProperties, properties, deviceProperties); info.Udid = GetString ("udid", newHardwareProperties, properties, hardwareProperties); info.DeviceClass = GetString ("deviceType", newHardwareProperties, properties, hardwareProperties); info.HardwareModel = GetString ("hardwareModel", newHardwareProperties, properties, hardwareProperties); @@ -67,9 +75,10 @@ public static List ParseDevices (string? json, ICustomLogger info.SerialNumber = GetString ("serialNumber", newHardwareProperties, properties, hardwareProperties); info.UniqueChipID = GetUInt64 ("ecid", newHardwareProperties, properties, hardwareProperties); - var cpuType = GetObject (newHardwareProperties, "cpuType") ?? GetObject (properties, "cpuType") ?? GetObject (hardwareProperties, "cpuType"); - if (cpuType.HasValue) - info.CpuArchitecture = GetString (cpuType.Value, "name"); + var newCpuType = GetObject (newHardwareProperties, "cpuType"); + var cpuType = GetObject (properties, "cpuType"); + var legacyCpuType = GetObject (hardwareProperties, "cpuType"); + info.CpuArchitecture = GetString ("name", newCpuType, cpuType, legacyCpuType); info.TransportType = GetString ("transportType", newConnectionProperties, stateProperties, properties, connectionProperties); info.PairingState = GetString ("pairingState", newConnectionProperties, stateProperties, properties, connectionProperties); diff --git a/tests/DeviceCtlOutputParserTests.cs b/tests/DeviceCtlOutputParserTests.cs index 274b51e..c7274b0 100644 --- a/tests/DeviceCtlOutputParserTests.cs +++ b/tests/DeviceCtlOutputParserTests.cs @@ -75,28 +75,45 @@ public void ParseDevices_JsonVersion5_ParsesProperties () ""deprecatedFields"": [ ""hardwareProperties"", ""deviceProperties"", ""connectionProperties"" ], ""replacement"": ""properties"" }, + ""deviceProperties"": { + ""name"": ""Deprecated name"", + ""osBuildUpdate"": ""22A1"", + ""osVersionNumber"": ""17.0"" + }, + ""hardwareProperties"": { + ""cpuType"": { ""name"": ""arm64e"" }, + ""hardwareModel"": ""D83AP"" + }, ""identifier"": ""33333333-AAAA-BBBB-CCCC-DDDDDDDDDDDD"", ""properties"": { ""connection"": { + ""pairingState"": ""paired"", ""transportType"": ""localNetwork"" }, - ""device"": { - ""name"": ""Rolf's iPhone 15"" - }, ""hardware"": { - ""cpuType"": { ""name"": ""arm64e"" }, + ""cpuType"": { + ""subtype"": 2, + ""type"": 16777228 + }, ""deviceType"": ""iPhone"", ""ecid"": 12345678, - ""hardwareModel"": ""D83AP"", ""platform"": ""iOS"", ""productType"": ""iPhone16,1"", ""serialNumber"": ""SERIAL_1"", ""udid"": ""00008003-012301230123ABCD"" }, + ""software"": { + ""osBuildVersions"": { + ""buildVersion"": { + ""name"": ""23B85"" + } + }, + ""osVersionNumber"": { + ""stringValue"": ""18.1"" + } + }, ""state"": { - ""osBuildUpdate"": ""23B85"", - ""osVersionNumber"": ""18.1"", - ""pairingState"": ""paired"" + ""name"": ""Rolf's iPhone 15"" } } } From 1cdbe4b5a8c7427b4cfc23a16493ffc546fa8798 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Wed, 16 Sep 2026 18:00:01 +0200 Subject: [PATCH 3/4] Parse devicectl numeric CPU architecture Translate the Mach CPU type and subtype values in the Xcode 27 properties payload into the architecture names expected by PhysicalDeviceInfo. Preserve named architecture fallback for legacy and unknown payloads, and cover both arm64e and arm64 without deprecated CPU data. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 891a4a77-04cb-4497-8c29-83759604e389 --- Xamarin.MacDev/DeviceCtlOutputParser.cs | 31 ++++++++++++++++++++++++- tests/DeviceCtlOutputParserTests.cs | 30 +++++++++++++++++++++++- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/Xamarin.MacDev/DeviceCtlOutputParser.cs b/Xamarin.MacDev/DeviceCtlOutputParser.cs index 6344dfe..2a4cafb 100644 --- a/Xamarin.MacDev/DeviceCtlOutputParser.cs +++ b/Xamarin.MacDev/DeviceCtlOutputParser.cs @@ -78,7 +78,9 @@ public static List ParseDevices (string? json, ICustomLogger var newCpuType = GetObject (newHardwareProperties, "cpuType"); var cpuType = GetObject (properties, "cpuType"); var legacyCpuType = GetObject (hardwareProperties, "cpuType"); - info.CpuArchitecture = GetString ("name", newCpuType, cpuType, legacyCpuType); + info.CpuArchitecture = GetCpuArchitecture (newCpuType); + if (string.IsNullOrEmpty (info.CpuArchitecture)) + info.CpuArchitecture = GetString ("name", newCpuType, cpuType, legacyCpuType); info.TransportType = GetString ("transportType", newConnectionProperties, stateProperties, properties, connectionProperties); info.PairingState = GetString ("pairingState", newConnectionProperties, stateProperties, properties, connectionProperties); @@ -111,6 +113,33 @@ static string GetString (JsonElement element, string property) return ""; } + static string GetCpuArchitecture (JsonElement? cpuType) + { + if (!cpuType.HasValue || + !cpuType.Value.TryGetProperty ("type", out var typeElement) || + !typeElement.TryGetInt32 (out var type)) + return ""; + + switch (type) { + case 7: + return "i386"; + case 12: + return "arm"; + case 0x01000007: + return "x86_64"; + case 0x0100000c: + if (cpuType.Value.TryGetProperty ("subtype", out var subtypeElement) && + subtypeElement.TryGetInt32 (out var subtype) && + (subtype & 0xff) == 2) + return "arm64e"; + return "arm64"; + case 0x0200000c: + return "arm64_32"; + default: + return ""; + } + } + static JsonElement? GetObject (JsonElement? element, string property) { if (element.HasValue && element.Value.TryGetProperty (property, out var value) && value.ValueKind == JsonValueKind.Object) diff --git a/tests/DeviceCtlOutputParserTests.cs b/tests/DeviceCtlOutputParserTests.cs index c7274b0..af63d0b 100644 --- a/tests/DeviceCtlOutputParserTests.cs +++ b/tests/DeviceCtlOutputParserTests.cs @@ -81,7 +81,6 @@ public void ParseDevices_JsonVersion5_ParsesProperties () ""osVersionNumber"": ""17.0"" }, ""hardwareProperties"": { - ""cpuType"": { ""name"": ""arm64e"" }, ""hardwareModel"": ""D83AP"" }, ""identifier"": ""33333333-AAAA-BBBB-CCCC-DDDDDDDDDDDD"", @@ -141,6 +140,35 @@ public void ParseDevices_JsonVersion5_ParsesProperties () Assert.That (device.PairingState, Is.EqualTo ("paired")); } + [Test] + public void ParseDevices_JsonVersion5_Arm64CpuType_ParsesArchitecture () + { + var json = @"{ + ""info"": { + ""jsonVersion"": 5 + }, + ""result"": { + ""devices"": [ + { + ""identifier"": ""ID-1"", + ""properties"": { + ""hardware"": { + ""cpuType"": { + ""subtype"": 0, + ""type"": 16777228 + } + } + } + } + ] + } + }"; + + var result = DeviceCtlOutputParser.ParseDevices (json); + Assert.That (result.Count, Is.EqualTo (1)); + Assert.That (result [0].CpuArchitecture, Is.EqualTo ("arm64")); + } + [Test] public void ParseDevices_MultipleDevices_ParsesAll () { From 4d203af5234d1df80a43edf565fe1475f0995d85 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Wed, 16 Sep 2026 18:08:48 +0200 Subject: [PATCH 4/4] Handle unsigned devicectl CPU subtypes Read Xcode 27 CPU subtype values as UInt64 so sign-extended ARM64e values retain their low-byte architecture discriminator. Update the v5 regression fixture to use the exact unsigned value emitted by devicectl. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 891a4a77-04cb-4497-8c29-83759604e389 --- Xamarin.MacDev/DeviceCtlOutputParser.cs | 2 +- tests/DeviceCtlOutputParserTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Xamarin.MacDev/DeviceCtlOutputParser.cs b/Xamarin.MacDev/DeviceCtlOutputParser.cs index 2a4cafb..a04e65f 100644 --- a/Xamarin.MacDev/DeviceCtlOutputParser.cs +++ b/Xamarin.MacDev/DeviceCtlOutputParser.cs @@ -129,7 +129,7 @@ static string GetCpuArchitecture (JsonElement? cpuType) return "x86_64"; case 0x0100000c: if (cpuType.Value.TryGetProperty ("subtype", out var subtypeElement) && - subtypeElement.TryGetInt32 (out var subtype) && + subtypeElement.TryGetUInt64 (out var subtype) && (subtype & 0xff) == 2) return "arm64e"; return "arm64"; diff --git a/tests/DeviceCtlOutputParserTests.cs b/tests/DeviceCtlOutputParserTests.cs index af63d0b..d081dab 100644 --- a/tests/DeviceCtlOutputParserTests.cs +++ b/tests/DeviceCtlOutputParserTests.cs @@ -91,7 +91,7 @@ public void ParseDevices_JsonVersion5_ParsesProperties () }, ""hardware"": { ""cpuType"": { - ""subtype"": 2, + ""subtype"": 18446744071562067970, ""type"": 16777228 }, ""deviceType"": ""iPhone"",