diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs index e83e7120ff6..553ba08972f 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs @@ -148,23 +148,23 @@ XDocument CreateDefaultManifest () /// /// Manifest templates may use compat JNI names (e.g., "android.apptests.App") - /// but the trimmable path generates JCWs with CRC-based names (e.g., "crc64.../App"). + /// but the trimmable path generates JCWs with hashed package names (e.g., "crc64.../App"). /// This method rewrites any compat name references to the actual JCW name so the /// Android runtime can find the class. /// void RewriteCompatNames (XElement manifest, IReadOnlyList allPeers) { - // Build mapping: fully-qualified compat Java name → CRC Java name - var compatToCrc = new Dictionary (allPeers.Count, StringComparer.Ordinal); + // Build mapping: fully-qualified compat Java name → hashed Java name + var compatToHashed = new Dictionary (allPeers.Count, StringComparer.Ordinal); foreach (var peer in allPeers) { string javaName = JniSignatureHelper.JniNameToJavaName (peer.JavaName); string compatName = JniSignatureHelper.JniNameToJavaName (peer.CompatJniName); if (javaName != compatName) { - compatToCrc [compatName] = javaName; + compatToHashed [compatName] = javaName; } } - if (compatToCrc.Count == 0) { + if (compatToHashed.Count == 0) { return; } @@ -173,7 +173,7 @@ void RewriteCompatNames (XElement manifest, IReadOnlyList allPeers // - fully qualified ("com.example.app.MainActivity") // - relative to the manifest package, starting with '.' (".MainActivity") // - bare, with no '.' at all ("MainActivity"), also relative to the package - // Resolve to the fully-qualified form before the lookup, then write the CRC + // Resolve to the fully-qualified form before the lookup, then write the hashed // name back so duplicate detection later in the pipeline works correctly. var packageName = (string?) manifest.Attribute ("package") ?? ""; @@ -187,8 +187,8 @@ void RewriteCompatNames (XElement manifest, IReadOnlyList allPeers continue; } var resolved = ManifestNameResolver.Resolve (nameAttr.Value, packageName); - if (compatToCrc.TryGetValue (resolved, out var crcName)) { - nameAttr.Value = crcName; + if (compatToHashed.TryGetValue (resolved, out var hashedName)) { + nameAttr.Value = hashedName; } } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj index 249bdc8def1..99399656317 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj @@ -5,6 +5,7 @@ $(TargetFrameworkNETStandard) enable Nullable + true Microsoft.Android.Sdk.TrimmableTypeMap true ..\..\product.snk @@ -18,6 +19,8 @@ + + diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 08ee7cd337d..0f2e782a200 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -6,6 +6,7 @@ using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using System.Reflection.PortableExecutable; +using Java.Interop.Tools.JavaCallableWrappers; namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// @@ -16,8 +17,19 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// public sealed class JavaPeerScanner : IDisposable { + enum HashedPackageNamingPolicy { + Crc64, + LowercaseCrc64, + } + readonly Dictionary assemblyCache = new (StringComparer.Ordinal); readonly Dictionary<(string typeName, string assemblyName), ActivationCtorInfo> activationCtorCache = new (); + readonly HashedPackageNamingPolicy packageNamingPolicy; + + public JavaPeerScanner (string? packageNamingPolicy = null) + { + this.packageNamingPolicy = ParsePackageNamingPolicy (packageNamingPolicy); + } /// /// Resolves a type name + assembly name to a TypeDefinitionHandle + AssemblyIndex. @@ -913,7 +925,7 @@ static string GetJavaAccess (MethodAttributes access) return registerJniName; } - // Fall back to already-scanned results (component-attributed or CRC64-computed peers) + // Fall back to already-scanned results (component-attributed or hashed-package peers) if (results.TryGetValue (baseTypeName, out var basePeer)) { return basePeer.JavaName; } @@ -1331,12 +1343,12 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) /// /// Compute both JNI name and compat JNI name for a type without [Register] or component Name. - /// JNI name uses CRC64 hash of "namespace:assemblyName" for the package. + /// JNI name uses the selected package naming policy hash for "namespace:assemblyName". /// Compat JNI name uses the raw managed namespace (lowercased). /// If a declaring type has [Register], its JNI name is used as prefix for both. /// Generic backticks are replaced with _. /// - static (string jniName, string compatJniName) ComputeAutoJniNames (TypeDefinition typeDef, AssemblyIndex index) + (string jniName, string compatJniName) ComputeAutoJniNames (TypeDefinition typeDef, AssemblyIndex index) { var (typeName, parentJniName, ns) = ComputeTypeNameParts (typeDef, index); @@ -1345,7 +1357,7 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) return (name, name); } - var packageName = GetCrc64PackageName (ns, index.AssemblyName); + var packageName = GetHashedPackageName (ns, index.AssemblyName); var jniName = $"{packageName}/{typeName}"; string compatName = ns.Length == 0 @@ -1360,7 +1372,7 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) /// registered JNI name or the outermost namespace. /// Matches JavaNativeTypeManager.ToJniName behavior: walks up declaring types /// and if a parent has [Register] or a component attribute JNI name, uses that - /// as prefix instead of computing CRC64 from the namespace. + /// as prefix instead of computing hashed package names from the namespace. /// static (string typeName, string? parentJniName, string ns) ComputeTypeNameParts (TypeDefinition typeDef, AssemblyIndex index) { @@ -1465,16 +1477,26 @@ static void ParseConnectorDeclaringType (string? connector, out string declaring declaringAssemblyName = nextComma >= 0 ? rest.Substring (0, nextComma).Trim () : rest.Trim (); } - static string GetCrc64PackageName (string ns, string assemblyName) + string GetHashedPackageName (string ns, string assemblyName) { // Only Mono.Android preserves the namespace directly if (assemblyName == "Mono.Android") { return ns.ToLowerInvariant ().Replace ('.', '/'); } - var data = System.Text.Encoding.UTF8.GetBytes ($"{ns}:{assemblyName}"); - var hash = System.IO.Hashing.Crc64.Hash (data); - return $"crc64{BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant ()}"; + return packageNamingPolicy switch { + HashedPackageNamingPolicy.LowercaseCrc64 => "crc64" + ScannerHashingHelper.ToLegacyCrc64 (ns, assemblyName), + HashedPackageNamingPolicy.Crc64 => "crc64" + ScannerHashingHelper.ToCrc64 (ns, assemblyName), + _ => throw new InvalidOperationException ($"Unsupported package naming policy: {packageNamingPolicy}"), + }; + } + + static HashedPackageNamingPolicy ParsePackageNamingPolicy (string? packageNamingPolicy) + { + if (string.Equals (packageNamingPolicy, "LowercaseCrc64", StringComparison.OrdinalIgnoreCase)) { + return HashedPackageNamingPolicy.LowercaseCrc64; + } + return HashedPackageNamingPolicy.Crc64; } static string ExtractNamespace (string fullName) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs new file mode 100644 index 00000000000..b57c78bbb82 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs @@ -0,0 +1,98 @@ +using System; +using System.Buffers; +using Java.Interop.Tools.JavaCallableWrappers; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +internal static class ScannerHashingHelper +{ + internal static string ToLegacyCrc64 (string ns, string assemblyName) + { + int byteCount = GetNamespaceAssemblyUtf8ByteCount (ns, assemblyName); + byte[] rented = ArrayPool.Shared.Rent (byteCount); + try { + int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, rented.AsSpan (0, byteCount)); + ulong crc = ulong.MaxValue; + ulong length = 0; + Crc64Helper.HashCore (rented, 0, bytesWritten, ref crc, ref length); + Span hash = stackalloc byte [8]; + WriteUInt64LittleEndian (hash, crc ^ length); + return ToHexString (hash, lowercase: true); + } finally { + ArrayPool.Shared.Return (rented); + } + } + + internal static string ToCrc64 (string ns, string assemblyName) + { + const int stackallocThresholdBytes = 256; + int byteCount = GetNamespaceAssemblyUtf8ByteCount (ns, assemblyName); + Span utf8Buffer = byteCount <= stackallocThresholdBytes + ? stackalloc byte [stackallocThresholdBytes] + : new byte [byteCount]; + + int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, utf8Buffer.Slice (0, byteCount)); + Span hash = stackalloc byte [8]; + System.IO.Hashing.Crc64.Hash (utf8Buffer.Slice (0, bytesWritten), hash); + return ToHexString (hash, lowercase: true); + } + + static int GetNamespaceAssemblyUtf8ByteCount (string ns, string assemblyName) + { + return System.Text.Encoding.UTF8.GetByteCount (ns) + 1 + System.Text.Encoding.UTF8.GetByteCount (assemblyName); + } + + static unsafe int GetNamespaceAssemblyUtf8Bytes (string ns, string assemblyName, Span destination) + { + int bytesWritten = 0; + fixed (char* nsPtr = ns) + fixed (byte* destinationPtr = destination) { + bytesWritten += System.Text.Encoding.UTF8.GetBytes (nsPtr, ns.Length, destinationPtr, destination.Length); + } + + destination [bytesWritten++] = (byte) ':'; + + fixed (char* assemblyNamePtr = assemblyName) + fixed (byte* destinationPtr = destination) { + bytesWritten += System.Text.Encoding.UTF8.GetBytes (assemblyNamePtr, assemblyName.Length, destinationPtr + bytesWritten, destination.Length - bytesWritten); + } + + return bytesWritten; + } + + static string ToHexString (ReadOnlySpan hash, bool lowercase) + { + const int maxStackCharLength = 128; + int charLength = hash.Length * 2; + Span chars = charLength <= maxStackCharLength + ? stackalloc char [charLength] + : new char [charLength]; + + for (int i = 0, j = 0; i < hash.Length; i += 1, j += 2) { + byte b = hash [i]; + chars [j] = GetHexValue (b / 16, lowercase); + chars [j + 1] = GetHexValue (b % 16, lowercase); + } + + return ((ReadOnlySpan) chars).ToString (); + } + + static void WriteUInt64LittleEndian (Span destination, ulong value) + { + destination [0] = (byte) value; + destination [1] = (byte) (value >> 8); + destination [2] = (byte) (value >> 16); + destination [3] = (byte) (value >> 24); + destination [4] = (byte) (value >> 32); + destination [5] = (byte) (value >> 40); + destination [6] = (byte) (value >> 48); + destination [7] = (byte) (value >> 56); + } + + static char GetHexValue (int value, bool lowercase) + { + return (char) (value < 10 + ? value + '0' + : value - 10 + (lowercase ? 'a' : 'A')); + } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 5d14490503a..d877a70b028 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -27,13 +27,14 @@ public TrimmableTypeMapResult Execute ( HashSet frameworkAssemblyNames, bool useSharedTypemapUniverse = false, ManifestConfig? manifestConfig = null, - XDocument? manifestTemplate = null) + XDocument? manifestTemplate = null, + string? packageNamingPolicy = null) { _ = assemblies ?? throw new ArgumentNullException (nameof (assemblies)); _ = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion)); _ = frameworkAssemblyNames ?? throw new ArgumentNullException (nameof (frameworkAssemblyNames)); - var (allPeers, assemblyManifestInfo) = ScanAssemblies (assemblies); + var (allPeers, assemblyManifestInfo) = ScanAssemblies (assemblies, packageNamingPolicy); if (allPeers.Count == 0) { logger.LogNoJavaPeerTypesFound (); return new TrimmableTypeMapResult ([], [], allPeers); @@ -104,9 +105,9 @@ GeneratedManifest GenerateManifest (List allPeers, AssemblyManifes return new GeneratedManifest (doc, providerNames.Count > 0 ? providerNames.ToArray () : []); } - (List peers, AssemblyManifestInfo manifestInfo) ScanAssemblies (IReadOnlyList<(string Name, PEReader Reader)> assemblies) + (List peers, AssemblyManifestInfo manifestInfo) ScanAssemblies (IReadOnlyList<(string Name, PEReader Reader)> assemblies, string? packageNamingPolicy) { - using var scanner = new JavaPeerScanner (); + using var scanner = new JavaPeerScanner (packageNamingPolicy); var peers = scanner.Scan (assemblies); var manifestInfo = scanner.ScanAssemblyManifestInfo (); logger.LogJavaPeerScanInfo (assemblies.Count, peers.Count); diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets index 52ce49be5cf..04135852229 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets @@ -11,6 +11,8 @@ <_TypeMapAssemblyName>_Microsoft.Android.TypeMaps + <_TrimmableTypeMapPackageNamingPolicy Condition=" '$(_AndroidPackageNamingPolicySetByUser)' == 'true' ">$(AndroidPackageNamingPolicy) + <_TrimmableTypeMapPackageNamingPolicy Condition=" '$(_TrimmableTypeMapPackageNamingPolicy)' == '' ">Crc64 @@ -73,6 +75,7 @@ Debug="$(AndroidIncludeDebugSymbols)" NeedsInternet="$(AndroidNeedsInternetPermission)" EmbedAssemblies="$(EmbedAssembliesIntoApk)" + PackageNamingPolicy="$(_TrimmableTypeMapPackageNamingPolicy)" ManifestPlaceholders="$(AndroidManifestPlaceholders)" CheckedBuild="$(_AndroidCheckedBuild)" ApplicationJavaClass="$(AndroidApplicationJavaClass)" diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index f80a14157f1..cb4d51021d3 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -68,6 +68,7 @@ public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) => public bool Debug { get; set; } public bool NeedsInternet { get; set; } public bool EmbedAssemblies { get; set; } + public string? PackageNamingPolicy { get; set; } public string? ManifestPlaceholders { get; set; } public string? CheckedBuild { get; set; } public string? ApplicationJavaClass { get; set; } @@ -131,7 +132,8 @@ public override bool RunTask () frameworkAssemblyNames, useSharedTypemapUniverse: !Debug, manifestConfig, - manifestTemplate); + manifestTemplate, + PackageNamingPolicy); GeneratedAssemblies = WriteAssembliesToDisk (result.GeneratedAssemblies, assemblyPaths); GeneratedJavaFiles = WriteJavaSourcesToDisk (result.GeneratedJavaSources); diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.props.in b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.props.in index 02589ee9384..b13f3abe411 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.props.in +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.props.in @@ -21,6 +21,7 @@ {abi}{versionCode:D5} UpdateGeneratedFiles True + <_AndroidPackageNamingPolicySetByUser Condition=" '$(AndroidPackageNamingPolicy)' != '' ">true LowercaseCrc64 False @BUNDLETOOL_VERSION@ diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.Helpers.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.Helpers.cs index 5014fe28a4c..c08c1b95f7e 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.Helpers.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.Helpers.cs @@ -72,12 +72,12 @@ static string[]? AllUserTypesAssemblyPaths { } } - static string NormalizeCrc64 (string javaName) + static string NormalizeHashedPackageName (string javaName) { - if (javaName.StartsWith ("crc64", StringComparison.Ordinal)) { + if (javaName.StartsWith ("crc64", StringComparison.Ordinal) || javaName.StartsWith ("xx64", StringComparison.Ordinal)) { int slash = javaName.IndexOf ('/'); if (slash > 0) { - return "crc64.../" + javaName.Substring (slash + 1); + return "hash.../" + javaName.Substring (slash + 1); } } return javaName; diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs index fcfe51cb1df..1628af0140c 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs @@ -115,8 +115,8 @@ public void ExactTypeMap_UserTypesFixture () var fixturePath = paths! [0]; var (legacy, _) = ScannerRunner.RunLegacy (fixturePath); var (newEntries, _) = ScannerRunner.RunNew (paths); -var legacyNormalized = legacy.Select (e => e with { JavaName = NormalizeCrc64 (e.JavaName) }).ToList (); -var newNormalized = newEntries.Select (e => e with { JavaName = NormalizeCrc64 (e.JavaName) }).ToList (); +var legacyNormalized = legacy.Select (e => e with { JavaName = NormalizeHashedPackageName (e.JavaName) }).ToList (); +var newNormalized = newEntries.Select (e => e with { JavaName = NormalizeHashedPackageName (e.JavaName) }).ToList (); AssertTypeMapMatch (legacyNormalized, newNormalized); } @@ -132,9 +132,9 @@ public void ExactMarshalMethods_UserTypesFixture () var (_, newMethods) = ScannerRunner.RunNew (paths); var legacyNormalized = legacyMethods -.ToDictionary (kvp => NormalizeCrc64 (kvp.Key), kvp => kvp.Value); +.ToDictionary (kvp => NormalizeHashedPackageName (kvp.Key), kvp => kvp.Value); var newNormalized = newMethods -.ToDictionary (kvp => NormalizeCrc64 (kvp.Key), kvp => kvp.Value); +.ToDictionary (kvp => NormalizeHashedPackageName (kvp.Key), kvp => kvp.Value); var result = MarshalMethodDiffHelper.CompareUserTypeMarshalMethods (legacyNormalized, newNormalized); AssertNoDiffs ("MISSING from new scanner", result.Missing); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index 98b28be0591..bf997025616 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -36,6 +36,18 @@ private protected static string TestFixtureAssemblyPath { private protected static List ScanFixtures () => _cachedScanResult.Value.peers; + private protected static List ScanFixtures (string packageNamingPolicy) + { + using var scanner = new JavaPeerScanner (packageNamingPolicy); + var peReader = new PEReader (File.OpenRead (TestFixtureAssemblyPath)); + var mdReader = peReader.GetMetadataReader (); + var assemblyName = mdReader.GetString (mdReader.GetAssemblyDefinition ().Name); + var assemblies = new [] { (assemblyName, peReader) }; + var peers = scanner.Scan (assemblies); + peReader.Dispose (); + return peers; + } + private protected static AssemblyManifestInfo ScanAssemblyManifestInfo () => _cachedScanResult.Value.manifestInfo; private protected static JavaPeerInfo FindFixtureByJavaName (string javaName) @@ -54,6 +66,14 @@ private protected static JavaPeerInfo FindFixtureByManagedName (string managedNa return peer; } + private protected static JavaPeerInfo FindFixtureByManagedName (string managedName, string packageNamingPolicy) + { + var peers = ScanFixtures (packageNamingPolicy); + var peer = peers.FirstOrDefault (p => p.ManagedTypeName == managedName); + Assert.NotNull (peer); + return peer; + } + static (string ns, string shortName) ParseManagedTypeName (string managedName) { var ns = managedName.Contains ('.') ? managedName.Substring (0, managedName.LastIndexOf ('.')) : ""; diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index bd9d1ab6b0d..b8fc18064d0 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -267,7 +267,7 @@ public void Build_PeerWithInvoker_CreatesProxy () [InlineData ("MyApp.UnregisteredExporter")] [InlineData ("MyApp.UnregisteredHelper")] [InlineData ("MyApp.DerivedFromComponentBase")] - public void Build_Crc64RenamedPeer_StoresFinalJavaNameOnProxy (string managedName) + public void Build_HashedRenamedPeer_StoresFinalJavaNameOnProxy (string managedName) { var peer = FindFixtureByManagedName (managedName); Assert.StartsWith ("crc64", peer.JavaName); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs index b1f96d6d320..6db3290c015 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs @@ -54,7 +54,7 @@ public void Scan_EmptyNamespace_Handled () [InlineData ("MyApp.UnnamedActivity")] [InlineData ("MyApp.UnregisteredClickListener")] [InlineData ("MyApp.UnregisteredExporter")] - public void Scan_UnregisteredType_DiscoveredWithCrc64Name (string managedName) + public void Scan_UnregisteredType_DiscoveredWithHashedName (string managedName) { Assert.StartsWith ("crc64", FindFixtureByManagedName (managedName).JavaName); } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs index a3b725caecb..8be0d078cf4 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs @@ -88,11 +88,25 @@ public void Scan_RegisterAttribute_DotFormat_NormalizedToSlashes () } [Theory] - [InlineData ("MyApp.PlainActivitySubclass", "crc64eb3df85c64aa1af6/PlainActivitySubclass")] - [InlineData ("MyApp.UnregisteredClickListener", "crc64eb3df85c64aa1af6/UnregisteredClickListener")] - [InlineData ("MyApp.UnregisteredExporter", "crc64eb3df85c64aa1af6/UnregisteredExporter")] - public void Scan_UnregisteredType_UsesCrc64PackageName (string managedName, string expectedJavaName) + [InlineData ("MyApp.PlainActivitySubclass", "PlainActivitySubclass")] + [InlineData ("MyApp.UnregisteredClickListener", "UnregisteredClickListener")] + [InlineData ("MyApp.UnregisteredExporter", "UnregisteredExporter")] + public void Scan_UnregisteredType_UsesHashedPackageName (string managedName, string expectedJavaName) { - Assert.Equal (expectedJavaName, FindFixtureByManagedName (managedName).JavaName); + Assert.Equal ($"crc64{ScannerHashingHelper.ToCrc64 ("MyApp", "TestFixtures")}/{expectedJavaName}", + FindFixtureByManagedName (managedName).JavaName); } + + [Fact] + public void Scan_UnregisteredType_Crc64Default_DiffersFromLegacyLowercaseCrc64Policy () + { + const string managedName = "MyApp.PlainActivitySubclass"; + var withCrc64 = FindFixtureByManagedName (managedName).JavaName; + var withLowercaseCrc64 = FindFixtureByManagedName (managedName, "LowercaseCrc64").JavaName; + + Assert.Equal ($"crc64{ScannerHashingHelper.ToCrc64 ("MyApp", "TestFixtures")}/PlainActivitySubclass", withCrc64); + Assert.Equal ("crc64ec59e927bc71f4d8/PlainActivitySubclass", withLowercaseCrc64); + Assert.NotEqual (withCrc64, withLowercaseCrc64); + } + } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ScannerHashingHelperTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ScannerHashingHelperTests.cs new file mode 100644 index 00000000000..d6fdb902b7a --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ScannerHashingHelperTests.cs @@ -0,0 +1,24 @@ +using Xunit; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; + +public class ScannerHashingHelperTests +{ + [Theory] + [InlineData ("MyApp", "TestFixtures", "ec59e927bc71f4d8")] + [InlineData ("System.Collections.Generic", "My.Assembly", "9ff866e93b19f500")] + [InlineData ("Hello", "World", "f6bdbfa73a558c54")] + public void ToLegacyCrc64_KnownInputs_HaveStableOutput (string ns, string assemblyName, string expected) + { + Assert.Equal (expected, ScannerHashingHelper.ToLegacyCrc64 (ns, assemblyName)); + } + + [Theory] + [InlineData ("MyApp", "TestFixtures", "eb3df85c64aa1af6")] + [InlineData ("System.Collections.Generic", "My.Assembly", "403b37c9b3a5014d")] + [InlineData ("Hello", "World", "4f2a517f331e7a2c")] + public void ToCrc64_KnownInputs_HaveStableOutput (string ns, string assemblyName, string expected) + { + Assert.Equal (expected, ScannerHashingHelper.ToCrc64 (ns, assemblyName)); + } +}