diff --git a/Documentation/docs-mobile/messages/index.md b/Documentation/docs-mobile/messages/index.md index 8c4ea0192de..f7f099ef1ab 100644 --- a/Documentation/docs-mobile/messages/index.md +++ b/Documentation/docs-mobile/messages/index.md @@ -227,7 +227,7 @@ Either change the value in the AndroidManifest.xml to match the $(SupportedOSPla + [XA4254](xa4254.md): Trimmable type map Java source input directory '{input}' and output directory '{output}' must be different. + [XA4255](xa4255.md): Generated trimmable type map Java source '{path}' was not found. + [XA4256](xa4256.md): Skipping Java peer type '{type}' from assembly '{assembly}' because referenced type '{referencedType}' from assembly '{referencedAssembly}' could not be resolved in '{path}'. This type will not be included in the trimmable type map. -+ [XA4258](xa4258.md): Java name '{name}' contains reserved Java identifier '{identifier}'. Change the package or type name. ++ [XA4258](xa4258.md): Java name '{name}' contains invalid or unsupported Java identifier '{identifier}'. Change the package or type name. + XA4300: Native library '{library}' will not be bundled because it has an unsupported ABI. + [XA4301](xa4301.md): Apk already contains the item `xxx`. + [XA4302](xa4302.md): Unhandled exception merging \`AndroidManifest.xml\`: {ex} diff --git a/Documentation/docs-mobile/messages/xa4258.md b/Documentation/docs-mobile/messages/xa4258.md index c8239e58b6f..bf953bba5f7 100644 --- a/Documentation/docs-mobile/messages/xa4258.md +++ b/Documentation/docs-mobile/messages/xa4258.md @@ -11,15 +11,27 @@ f1_keywords: ## Example message ```text -error XA4258: Java name 'com.example.for' contains reserved Java identifier 'for'. Change the package or type name. +error XA4258: Java name 'com.example.for' contains invalid or unsupported Java identifier 'for'. Change the package or type name. ``` ## Issue -A Java package or type name contains a Java keyword or restricted type identifier. Java does not provide a way to escape keywords. Restricted type identifiers are also rejected to keep generated Java source compatible when .NET for Android moves to a newer Java source level. +A Java package or type name contains an invalid identifier, a Java keyword, or a restricted type +identifier. Java identifier classification follows Java 21's Unicode 15.0 data so it does not vary +with the .NET runtime used to build the app. + +.NET for Android requires Normalization Form C (NFC) names and rejects combining and format +characters because normalizing file systems can change or merge generated Java source paths. +Supplementary characters are rejected because Android's class loader cannot resolve classes with +supplementary characters in their simple name, even when the exact descriptor is present in DEX. This can originate from the `$(ApplicationId)` MSBuild property, the `package` attribute in `AndroidManifest.xml`, a managed type name, or an explicit Java name supplied by an attribute such as `[Register]` or `[JniTypeSignature]`. ## Solution -Change the package or type name so no segment is a Java keyword. For a type name, also avoid restricted type identifiers such as `record`. +Use an NFC name. Java type segments support Java 21 BMP identifier starts, including currency +symbols and connector punctuation, plus decimal digits after the first character. Package segments +and Android manifest component type names use the narrower Android-compatible set of BMP letters, +decimal digits after the first character, underscores, and dollar signs. Avoid combining, format, +and supplementary characters. No segment may be a Java keyword; type names must also avoid +restricted identifiers such as `record`. diff --git a/external/Java.Interop/src/Xamarin.Android.Tools.Bytecode/ConstantPool.cs b/external/Java.Interop/src/Xamarin.Android.Tools.Bytecode/ConstantPool.cs index 946898ec752..9126c803ee6 100644 --- a/external/Java.Interop/src/Xamarin.Android.Tools.Bytecode/ConstantPool.cs +++ b/external/Java.Interop/src/Xamarin.Android.Tools.Bytecode/ConstantPool.cs @@ -384,31 +384,44 @@ public ConstantPoolUtf8Item (ConstantPool constantPool, Stream stream) for (int i = 0; i < data.Length; ++i) data [i] = stream.ReadNetworkByte (); - // The .class file specially encodes NUL so that it takes 2 bytes, not 1. - // http://en.wikipedia.org/wiki/UTF-8#Modified_UTF-8 - var fixup = new List (data.Length); + // Modified UTF-8 encodes UTF-16 code units, so supplementary characters remain + // a high-surrogate/low-surrogate pair after decoding. + var decoded = new StringBuilder (data.Length); for (int i = 0; i < data.Length; ++i) { - if (data [i] == 0xc0 && (i + 1) < data.Length && data [i + 1] == 0x80) { - fixup.Add (0x00); - i++; + byte first = data [i]; + if ((first & 0x80) == 0) { + if (first == 0) + throw new InvalidDataException ("Modified UTF-8 contains a raw null byte."); + decoded.Append ((char) first); continue; } - // ...and they couldn't be bothered with supporting 4-byte UTF-8 sequences, - // needed for Emoji and chars off the Basic Multilingual Plane; instead, they're - // encoded as a surrogate pair. (What is this I don't even...) - if (data [i] == 0xed && i+6 < data.Length && data [i+3] == 0xed) { - var surrogatePair = new char [] { - (char) (0xD800 + (((data [i+1] & 0x0F) << 6) | (data [i+2] & 0x3F))), - (char) (0xDC00 + (((data [i+4] & 0x0F) << 6) | (data [i+5] & 0x3F))), - }; - fixup.AddRange (Encoding.UTF8.GetBytes (surrogatePair)); - i += 5; + if ((first & 0xe0) == 0xc0 && i + 1 < data.Length) { + byte second = ReadContinuationByte (data [++i]); + char value = (char) (((first & 0x1f) << 6) | (second & 0x3f)); + if (value < '\u0080' && (first != 0xc0 || second != 0x80)) + throw new InvalidDataException ("Modified UTF-8 contains an invalid two-byte overlong encoding."); + decoded.Append (value); + continue; + } + if ((first & 0xf0) == 0xe0 && i + 2 < data.Length) { + byte second = ReadContinuationByte (data [++i]); + byte third = ReadContinuationByte (data [++i]); + char value = (char) (((first & 0x0f) << 12) | ((second & 0x3f) << 6) | (third & 0x3f)); + if (value < '\u0800') + throw new InvalidDataException ("Modified UTF-8 contains an invalid three-byte overlong encoding."); + decoded.Append (value); continue; - } - fixup.Add (data [i]); + throw new InvalidDataException ($"Invalid modified UTF-8 lead byte 0x{first:x2}."); } - value = Encoding.UTF8.GetString (fixup.Count == data.Length ? data : fixup.ToArray ()); + value = decoded.ToString (); + } + + static byte ReadContinuationByte (byte value) + { + if ((value & 0xc0) != 0x80) + throw new InvalidDataException ($"Invalid modified UTF-8 continuation byte 0x{value:x2}."); + return value; } public override ConstantPoolItemType Type { diff --git a/external/Java.Interop/tests/Xamarin.Android.Tools.Bytecode-Tests/ClassFileTests.cs b/external/Java.Interop/tests/Xamarin.Android.Tools.Bytecode-Tests/ClassFileTests.cs index 465b64ba06d..c2fc7955441 100644 --- a/external/Java.Interop/tests/Xamarin.Android.Tools.Bytecode-Tests/ClassFileTests.cs +++ b/external/Java.Interop/tests/Xamarin.Android.Tools.Bytecode-Tests/ClassFileTests.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Reflection; using Xamarin.Android.Tools.Bytecode; @@ -15,6 +16,52 @@ public void Constructor_Exceptions () { Assert.Throws (() => new ClassFile (null)); } + + [Test] + public void ModifiedUtf8_DecodesUtf16CodeUnits () + { + using var poolStream = new MemoryStream (new byte [] { 0, 1 }); + var pool = new ConstantPool (poolStream); + + Assert.AreEqual ("\0", Decode (pool, new byte [] { 0xc0, 0x80 })); + Assert.AreEqual ("\u0080\u0800", Decode (pool, new byte [] { 0xc2, 0x80, 0xe0, 0xa0, 0x80 })); + Assert.AreEqual ( + "\ud000\ud001", + Decode (pool, new byte [] { 0xed, 0x80, 0x80, 0xed, 0x80, 0x81 }) + ); + Assert.AreEqual ( + "\U00010400", + Decode (pool, new byte [] { 0xed, 0xa0, 0x81, 0xed, 0xb0, 0x80 }) + ); + Assert.AreEqual ( + "\ud801\udc00\ud801", + Decode (pool, new byte [] { 0xed, 0xa0, 0x81, 0xed, 0xb0, 0x80, 0xed, 0xa0, 0x81 }) + ); + } + + [Test] + public void ModifiedUtf8_RejectsMalformedEncodings () + { + using var poolStream = new MemoryStream (new byte [] { 0, 1 }); + var pool = new ConstantPool (poolStream); + foreach (var bytes in new [] { + new byte [] { 0 }, + new byte [] { 0xc0, 0x81 }, + new byte [] { 0xc1, 0xbf }, + new byte [] { 0xe0, 0x81, 0x81 }, + }) { + Assert.Throws (() => Decode (pool, bytes)); + } + } + + static string Decode (ConstantPool pool, byte [] bytes) + { + using var stream = new MemoryStream (); + stream.WriteByte ((byte) (bytes.Length >> 8)); + stream.WriteByte ((byte) bytes.Length); + stream.Write (bytes, 0, bytes.Length); + stream.Position = 0; + return new ConstantPoolUtf8Item (pool, stream).Value; + } } } - diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs index bc5cbd02220..23556c4eca0 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs @@ -84,7 +84,7 @@ static string GetRelativePath (JavaPeerInfo type) /// /// Validates that the JNI name is well-formed: non-empty, each segment separated by '/' - /// contains only valid Java identifier characters (letters, digits, '_', '$'). + /// is a supported NFC Java 21 identifier, and no segment is reserved. /// This also prevents path traversal (e.g., ".." segments, rooted paths, backslashes). /// static void WritePackageDeclaration (JavaPeerInfo type, TextWriter writer) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs index 849a4ec78b5..8d20d2bd13e 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs @@ -220,35 +220,8 @@ internal static void ValidateJniName (string jniName) throw new ArgumentException ("JNI name must not be null or empty.", nameof (jniName)); } - int segmentStart = 0; - for (int i = 0; i <= jniName.Length; i++) { - if (i == jniName.Length || jniName [i] == '/') { - if (i == segmentStart) { - throw new ArgumentException ($"JNI name '{jniName}' has an empty segment.", nameof (jniName)); - } - - // First char of a segment must not be a digit - char first = jniName [segmentStart]; - if (first >= '0' && first <= '9') { - throw new ArgumentException ($"JNI name '{jniName}' has a segment starting with a digit.", nameof (jniName)); - } - - // All chars in the segment must be valid Java identifier chars - for (int j = segmentStart; j < i; j++) { - char c = jniName [j]; - bool valid = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || - (c >= '0' && c <= '9') || c == '_' || c == '$'; - if (!valid) { - throw new ArgumentException ($"JNI name '{jniName}' contains invalid character '{c}'.", nameof (jniName)); - } - } - - segmentStart = i + 1; - } - } - if (JavaNameValidator.TryGetInvalidJniNameSegment (jniName, out var invalidIdentifier)) { - throw new ArgumentException ($"JNI name '{jniName}' contains reserved Java identifier '{invalidIdentifier}'.", nameof (jniName)); + throw new ArgumentException ($"JNI name '{jniName}' contains invalid or unsupported Java identifier '{invalidIdentifier}'.", nameof (jniName)); } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestConstants.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestConstants.cs index 191637babaa..6983d165891 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestConstants.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestConstants.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using System.Xml.Linq; namespace Microsoft.Android.Sdk.TrimmableTypeMap; @@ -6,4 +8,13 @@ static class ManifestConstants { public static readonly XNamespace AndroidNs = "http://schemas.android.com/apk/res/android"; public static readonly XName AttName = AndroidNs + "name"; + public static readonly HashSet ComponentElementNames = new (StringComparer.Ordinal) { + "application", + "activity", + "activity-alias", + "instrumentation", + "service", + "receiver", + "provider", + }; } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs index be0c06a838b..2a6a4b0da14 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs @@ -16,14 +16,6 @@ class ManifestGenerator static readonly XNamespace AndroidNs = ManifestConstants.AndroidNs; static readonly XName AttName = ManifestConstants.AttName; static readonly char [] PlaceholderSeparators = [';']; - static readonly HashSet ComponentElementNames = new (StringComparer.Ordinal) { - "application", - "activity", - "instrumentation", - "service", - "receiver", - "provider", - }; /// Warning code for library-manifest merge failures (maps to XA4302). internal const int LibraryManifestMergeWarningCode = 4302; @@ -60,6 +52,7 @@ class ManifestGenerator // own package when they are relative (start with '.'). Mirrors ManifestDocument.ManifestAttributeFixups. static readonly Dictionary ManifestAttributeFixups = new (StringComparer.Ordinal) { { "activity", ["name"] }, + { "activity-alias", ["name", "targetActivity"] }, { "application", ["backupAgent"] }, { "instrumentation", ["name"] }, { "provider", ["name"] }, @@ -83,6 +76,9 @@ class ManifestGenerator } EnsureManifestAttributes (manifest); + // Template component names must be resolved before compat-name rewriting. Apply again + // after library-manifest merging so placeholders introduced by libraries are also covered. + ApplyPlaceholders (doc, ManifestPlaceholders, PackageName); var app = EnsureApplicationElement (manifest); var targetSdkVersionValue = GetTargetSdkVersionValue (manifest); @@ -205,6 +201,7 @@ void MergeLibraryManifests (XElement manifest) continue; } + ApplyPlaceholders (libDoc, ManifestPlaceholders, PackageName); var package = (string?) libRoot.Attribute ("package") ?? ""; foreach (var top in libRoot.Elements ().ToList ()) { var name = (string?) top.Attribute (AndroidNs + "name"); @@ -320,20 +317,22 @@ void RewriteCompatNames (XElement manifest, IReadOnlyList allPeers continue; } - var nameAttr = element.Attribute (AttName); - if (nameAttr is null) { + var classNameAttr = element.Name.LocalName == "activity-alias" + ? element.Attribute (AndroidNs + "targetActivity") + : element.Attribute (AttName); + if (classNameAttr is null) { continue; } - var resolved = ManifestNameResolver.Resolve (nameAttr.Value, packageName); + var resolved = ManifestNameResolver.Resolve (classNameAttr.Value, packageName); if (compatToCrc.TryGetValue (resolved, out var crcName)) { - nameAttr.Value = crcName; + classNameAttr.Value = crcName; } } } static bool IsComponentElement (XElement element) { - return element.Name.NamespaceName.Length == 0 && ComponentElementNames.Contains (element.Name.LocalName); + return element.Name.NamespaceName.Length == 0 && ManifestConstants.ComponentElementNames.Contains (element.Name.LocalName); } void EnsureManifestAttributes (XElement manifest) @@ -345,10 +344,7 @@ void EnsureManifestAttributes (XElement manifest) // produced by GetAndroidPackageName, which substitutes placeholders and canonicalizes the // package). This matches the legacy GenerateMainAndroidManifest; a valid explicit package is // preserved so compat-name resolution keeps using it. - var packageAttr = (string?) manifest.Attribute ("package") ?? ""; - if ((packageAttr.Length == 0 || packageAttr.Contains ("${")) && !PackageName.IsNullOrEmpty ()) { - manifest.SetAttributeValue ("package", PackageName); - } + ResolvePackageName (manifest, PackageName); if (manifest.Attribute (AndroidNs + "versionCode") is null) { manifest.SetAttributeValue (AndroidNs + "versionCode", @@ -380,6 +376,14 @@ void EnsureManifestAttributes (XElement manifest) } } + internal static void ResolvePackageName (XElement manifest, string? packageName) + { + var packageAttr = (string?) manifest.Attribute ("package") ?? ""; + if ((packageAttr.Length == 0 || packageAttr.Contains ("${")) && !packageName.IsNullOrEmpty ()) { + manifest.SetAttributeValue ("package", packageName); + } + } + XElement EnsureApplicationElement (XElement manifest) { var app = manifest.Element ("application"); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/JavaIdentifierData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/JavaIdentifierData.cs new file mode 100644 index 00000000000..a0070728454 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/JavaIdentifierData.cs @@ -0,0 +1,581 @@ +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +// Generated from OpenJDK 21.0.12.1 java.lang.Character (Unicode 15.0.0). +// https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Character.html +// Adjacent pairs are BMP ranges; adjacent surrogate-pair pairs are supplementary ranges. +// SHA-256 of the 1,114,112 start/part flag bytes: +// 81b63b25dd80b36fcd964822c76bc18fe9c04319876d26b4ba3983f7d7090319 +static class JavaIdentifierData +{ + const string IdentifierStartRanges = + "\u0024\u0024\u0041\u005a\u005f\u005f\u0061\u007a\u00a2\u00a5\u00aa\u00aa\u00b5\u00b5\u00ba\u00ba" + + "\u00c0\u00d6\u00d8\u00f6\u00f8\u02c1\u02c6\u02d1\u02e0\u02e4\u02ec\u02ec\u02ee\u02ee\u0370\u0374" + + "\u0376\u0377\u037a\u037d\u037f\u037f\u0386\u0386\u0388\u038a\u038c\u038c\u038e\u03a1\u03a3\u03f5" + + "\u03f7\u0481\u048a\u052f\u0531\u0556\u0559\u0559\u0560\u0588\u058f\u058f\u05d0\u05ea\u05ef\u05f2" + + "\u060b\u060b\u0620\u064a\u066e\u066f\u0671\u06d3\u06d5\u06d5\u06e5\u06e6\u06ee\u06ef\u06fa\u06fc" + + "\u06ff\u06ff\u0710\u0710\u0712\u072f\u074d\u07a5\u07b1\u07b1\u07ca\u07ea\u07f4\u07f5\u07fa\u07fa" + + "\u07fe\u0815\u081a\u081a\u0824\u0824\u0828\u0828\u0840\u0858\u0860\u086a\u0870\u0887\u0889\u088e" + + "\u08a0\u08c9\u0904\u0939\u093d\u093d\u0950\u0950\u0958\u0961\u0971\u0980\u0985\u098c\u098f\u0990" + + "\u0993\u09a8\u09aa\u09b0\u09b2\u09b2\u09b6\u09b9\u09bd\u09bd\u09ce\u09ce\u09dc\u09dd\u09df\u09e1" + + "\u09f0\u09f3\u09fb\u09fc\u0a05\u0a0a\u0a0f\u0a10\u0a13\u0a28\u0a2a\u0a30\u0a32\u0a33\u0a35\u0a36" + + "\u0a38\u0a39\u0a59\u0a5c\u0a5e\u0a5e\u0a72\u0a74\u0a85\u0a8d\u0a8f\u0a91\u0a93\u0aa8\u0aaa\u0ab0" + + "\u0ab2\u0ab3\u0ab5\u0ab9\u0abd\u0abd\u0ad0\u0ad0\u0ae0\u0ae1\u0af1\u0af1\u0af9\u0af9\u0b05\u0b0c" + + "\u0b0f\u0b10\u0b13\u0b28\u0b2a\u0b30\u0b32\u0b33\u0b35\u0b39\u0b3d\u0b3d\u0b5c\u0b5d\u0b5f\u0b61" + + "\u0b71\u0b71\u0b83\u0b83\u0b85\u0b8a\u0b8e\u0b90\u0b92\u0b95\u0b99\u0b9a\u0b9c\u0b9c\u0b9e\u0b9f" + + "\u0ba3\u0ba4\u0ba8\u0baa\u0bae\u0bb9\u0bd0\u0bd0\u0bf9\u0bf9\u0c05\u0c0c\u0c0e\u0c10\u0c12\u0c28" + + "\u0c2a\u0c39\u0c3d\u0c3d\u0c58\u0c5a\u0c5d\u0c5d\u0c60\u0c61\u0c80\u0c80\u0c85\u0c8c\u0c8e\u0c90" + + "\u0c92\u0ca8\u0caa\u0cb3\u0cb5\u0cb9\u0cbd\u0cbd\u0cdd\u0cde\u0ce0\u0ce1\u0cf1\u0cf2\u0d04\u0d0c" + + "\u0d0e\u0d10\u0d12\u0d3a\u0d3d\u0d3d\u0d4e\u0d4e\u0d54\u0d56\u0d5f\u0d61\u0d7a\u0d7f\u0d85\u0d96" + + "\u0d9a\u0db1\u0db3\u0dbb\u0dbd\u0dbd\u0dc0\u0dc6\u0e01\u0e30\u0e32\u0e33\u0e3f\u0e46\u0e81\u0e82" + + "\u0e84\u0e84\u0e86\u0e8a\u0e8c\u0ea3\u0ea5\u0ea5\u0ea7\u0eb0\u0eb2\u0eb3\u0ebd\u0ebd\u0ec0\u0ec4" + + "\u0ec6\u0ec6\u0edc\u0edf\u0f00\u0f00\u0f40\u0f47\u0f49\u0f6c\u0f88\u0f8c\u1000\u102a\u103f\u103f" + + "\u1050\u1055\u105a\u105d\u1061\u1061\u1065\u1066\u106e\u1070\u1075\u1081\u108e\u108e\u10a0\u10c5" + + "\u10c7\u10c7\u10cd\u10cd\u10d0\u10fa\u10fc\u1248\u124a\u124d\u1250\u1256\u1258\u1258\u125a\u125d" + + "\u1260\u1288\u128a\u128d\u1290\u12b0\u12b2\u12b5\u12b8\u12be\u12c0\u12c0\u12c2\u12c5\u12c8\u12d6" + + "\u12d8\u1310\u1312\u1315\u1318\u135a\u1380\u138f\u13a0\u13f5\u13f8\u13fd\u1401\u166c\u166f\u167f" + + "\u1681\u169a\u16a0\u16ea\u16ee\u16f8\u1700\u1711\u171f\u1731\u1740\u1751\u1760\u176c\u176e\u1770" + + "\u1780\u17b3\u17d7\u17d7\u17db\u17dc\u1820\u1878\u1880\u1884\u1887\u18a8\u18aa\u18aa\u18b0\u18f5" + + "\u1900\u191e\u1950\u196d\u1970\u1974\u1980\u19ab\u19b0\u19c9\u1a00\u1a16\u1a20\u1a54\u1aa7\u1aa7" + + "\u1b05\u1b33\u1b45\u1b4c\u1b83\u1ba0\u1bae\u1baf\u1bba\u1be5\u1c00\u1c23\u1c4d\u1c4f\u1c5a\u1c7d" + + "\u1c80\u1c88\u1c90\u1cba\u1cbd\u1cbf\u1ce9\u1cec\u1cee\u1cf3\u1cf5\u1cf6\u1cfa\u1cfa\u1d00\u1dbf" + + "\u1e00\u1f15\u1f18\u1f1d\u1f20\u1f45\u1f48\u1f4d\u1f50\u1f57\u1f59\u1f59\u1f5b\u1f5b\u1f5d\u1f5d" + + "\u1f5f\u1f7d\u1f80\u1fb4\u1fb6\u1fbc\u1fbe\u1fbe\u1fc2\u1fc4\u1fc6\u1fcc\u1fd0\u1fd3\u1fd6\u1fdb" + + "\u1fe0\u1fec\u1ff2\u1ff4\u1ff6\u1ffc\u203f\u2040\u2054\u2054\u2071\u2071\u207f\u207f\u2090\u209c" + + "\u20a0\u20c0\u2102\u2102\u2107\u2107\u210a\u2113\u2115\u2115\u2119\u211d\u2124\u2124\u2126\u2126" + + "\u2128\u2128\u212a\u212d\u212f\u2139\u213c\u213f\u2145\u2149\u214e\u214e\u2160\u2188\u2c00\u2ce4" + + "\u2ceb\u2cee\u2cf2\u2cf3\u2d00\u2d25\u2d27\u2d27\u2d2d\u2d2d\u2d30\u2d67\u2d6f\u2d6f\u2d80\u2d96" + + "\u2da0\u2da6\u2da8\u2dae\u2db0\u2db6\u2db8\u2dbe\u2dc0\u2dc6\u2dc8\u2dce\u2dd0\u2dd6\u2dd8\u2dde" + + "\u2e2f\u2e2f\u3005\u3007\u3021\u3029\u3031\u3035\u3038\u303c\u3041\u3096\u309d\u309f\u30a1\u30fa" + + "\u30fc\u30ff\u3105\u312f\u3131\u318e\u31a0\u31bf\u31f0\u31ff\u3400\u4dbf\u4e00\ua48c\ua4d0\ua4fd" + + "\ua500\ua60c\ua610\ua61f\ua62a\ua62b\ua640\ua66e\ua67f\ua69d\ua6a0\ua6ef\ua717\ua71f\ua722\ua788" + + "\ua78b\ua7ca\ua7d0\ua7d1\ua7d3\ua7d3\ua7d5\ua7d9\ua7f2\ua801\ua803\ua805\ua807\ua80a\ua80c\ua822" + + "\ua838\ua838\ua840\ua873\ua882\ua8b3\ua8f2\ua8f7\ua8fb\ua8fb\ua8fd\ua8fe\ua90a\ua925\ua930\ua946" + + "\ua960\ua97c\ua984\ua9b2\ua9cf\ua9cf\ua9e0\ua9e4\ua9e6\ua9ef\ua9fa\ua9fe\uaa00\uaa28\uaa40\uaa42" + + "\uaa44\uaa4b\uaa60\uaa76\uaa7a\uaa7a\uaa7e\uaaaf\uaab1\uaab1\uaab5\uaab6\uaab9\uaabd\uaac0\uaac0" + + "\uaac2\uaac2\uaadb\uaadd\uaae0\uaaea\uaaf2\uaaf4\uab01\uab06\uab09\uab0e\uab11\uab16\uab20\uab26" + + "\uab28\uab2e\uab30\uab5a\uab5c\uab69\uab70\uabe2\uac00\ud7a3\ud7b0\ud7c6\ud7cb\ud7fb\uf900\ufa6d" + + "\ufa70\ufad9\ufb00\ufb06\ufb13\ufb17\ufb1d\ufb1d\ufb1f\ufb28\ufb2a\ufb36\ufb38\ufb3c\ufb3e\ufb3e" + + "\ufb40\ufb41\ufb43\ufb44\ufb46\ufbb1\ufbd3\ufd3d\ufd50\ufd8f\ufd92\ufdc7\ufdf0\ufdfc\ufe33\ufe34" + + "\ufe4d\ufe4f\ufe69\ufe69\ufe70\ufe74\ufe76\ufefc\uff04\uff04\uff21\uff3a\uff3f\uff3f\uff41\uff5a" + + "\uff66\uffbe\uffc2\uffc7\uffca\uffcf\uffd2\uffd7\uffda\uffdc\uffe0\uffe1\uffe5\uffe6"; + + const string IdentifierPartRanges = + "\u0000\u0008\u000e\u001b\u0024\u0024\u0030\u0039\u0041\u005a\u005f\u005f\u0061\u007a\u007f\u009f" + + "\u00a2\u00a5\u00aa\u00aa\u00ad\u00ad\u00b5\u00b5\u00ba\u00ba\u00c0\u00d6\u00d8\u00f6\u00f8\u02c1" + + "\u02c6\u02d1\u02e0\u02e4\u02ec\u02ec\u02ee\u02ee\u0300\u0374\u0376\u0377\u037a\u037d\u037f\u037f" + + "\u0386\u0386\u0388\u038a\u038c\u038c\u038e\u03a1\u03a3\u03f5\u03f7\u0481\u0483\u0487\u048a\u052f" + + "\u0531\u0556\u0559\u0559\u0560\u0588\u058f\u058f\u0591\u05bd\u05bf\u05bf\u05c1\u05c2\u05c4\u05c5" + + "\u05c7\u05c7\u05d0\u05ea\u05ef\u05f2\u0600\u0605\u060b\u060b\u0610\u061a\u061c\u061c\u0620\u0669" + + "\u066e\u06d3\u06d5\u06dd\u06df\u06e8\u06ea\u06fc\u06ff\u06ff\u070f\u074a\u074d\u07b1\u07c0\u07f5" + + "\u07fa\u07fa\u07fd\u082d\u0840\u085b\u0860\u086a\u0870\u0887\u0889\u088e\u0890\u0891\u0898\u0963" + + "\u0966\u096f\u0971\u0983\u0985\u098c\u098f\u0990\u0993\u09a8\u09aa\u09b0\u09b2\u09b2\u09b6\u09b9" + + "\u09bc\u09c4\u09c7\u09c8\u09cb\u09ce\u09d7\u09d7\u09dc\u09dd\u09df\u09e3\u09e6\u09f3\u09fb\u09fc" + + "\u09fe\u09fe\u0a01\u0a03\u0a05\u0a0a\u0a0f\u0a10\u0a13\u0a28\u0a2a\u0a30\u0a32\u0a33\u0a35\u0a36" + + "\u0a38\u0a39\u0a3c\u0a3c\u0a3e\u0a42\u0a47\u0a48\u0a4b\u0a4d\u0a51\u0a51\u0a59\u0a5c\u0a5e\u0a5e" + + "\u0a66\u0a75\u0a81\u0a83\u0a85\u0a8d\u0a8f\u0a91\u0a93\u0aa8\u0aaa\u0ab0\u0ab2\u0ab3\u0ab5\u0ab9" + + "\u0abc\u0ac5\u0ac7\u0ac9\u0acb\u0acd\u0ad0\u0ad0\u0ae0\u0ae3\u0ae6\u0aef\u0af1\u0af1\u0af9\u0aff" + + "\u0b01\u0b03\u0b05\u0b0c\u0b0f\u0b10\u0b13\u0b28\u0b2a\u0b30\u0b32\u0b33\u0b35\u0b39\u0b3c\u0b44" + + "\u0b47\u0b48\u0b4b\u0b4d\u0b55\u0b57\u0b5c\u0b5d\u0b5f\u0b63\u0b66\u0b6f\u0b71\u0b71\u0b82\u0b83" + + "\u0b85\u0b8a\u0b8e\u0b90\u0b92\u0b95\u0b99\u0b9a\u0b9c\u0b9c\u0b9e\u0b9f\u0ba3\u0ba4\u0ba8\u0baa" + + "\u0bae\u0bb9\u0bbe\u0bc2\u0bc6\u0bc8\u0bca\u0bcd\u0bd0\u0bd0\u0bd7\u0bd7\u0be6\u0bef\u0bf9\u0bf9" + + "\u0c00\u0c0c\u0c0e\u0c10\u0c12\u0c28\u0c2a\u0c39\u0c3c\u0c44\u0c46\u0c48\u0c4a\u0c4d\u0c55\u0c56" + + "\u0c58\u0c5a\u0c5d\u0c5d\u0c60\u0c63\u0c66\u0c6f\u0c80\u0c83\u0c85\u0c8c\u0c8e\u0c90\u0c92\u0ca8" + + "\u0caa\u0cb3\u0cb5\u0cb9\u0cbc\u0cc4\u0cc6\u0cc8\u0cca\u0ccd\u0cd5\u0cd6\u0cdd\u0cde\u0ce0\u0ce3" + + "\u0ce6\u0cef\u0cf1\u0cf3\u0d00\u0d0c\u0d0e\u0d10\u0d12\u0d44\u0d46\u0d48\u0d4a\u0d4e\u0d54\u0d57" + + "\u0d5f\u0d63\u0d66\u0d6f\u0d7a\u0d7f\u0d81\u0d83\u0d85\u0d96\u0d9a\u0db1\u0db3\u0dbb\u0dbd\u0dbd" + + "\u0dc0\u0dc6\u0dca\u0dca\u0dcf\u0dd4\u0dd6\u0dd6\u0dd8\u0ddf\u0de6\u0def\u0df2\u0df3\u0e01\u0e3a" + + "\u0e3f\u0e4e\u0e50\u0e59\u0e81\u0e82\u0e84\u0e84\u0e86\u0e8a\u0e8c\u0ea3\u0ea5\u0ea5\u0ea7\u0ebd" + + "\u0ec0\u0ec4\u0ec6\u0ec6\u0ec8\u0ece\u0ed0\u0ed9\u0edc\u0edf\u0f00\u0f00\u0f18\u0f19\u0f20\u0f29" + + "\u0f35\u0f35\u0f37\u0f37\u0f39\u0f39\u0f3e\u0f47\u0f49\u0f6c\u0f71\u0f84\u0f86\u0f97\u0f99\u0fbc" + + "\u0fc6\u0fc6\u1000\u1049\u1050\u109d\u10a0\u10c5\u10c7\u10c7\u10cd\u10cd\u10d0\u10fa\u10fc\u1248" + + "\u124a\u124d\u1250\u1256\u1258\u1258\u125a\u125d\u1260\u1288\u128a\u128d\u1290\u12b0\u12b2\u12b5" + + "\u12b8\u12be\u12c0\u12c0\u12c2\u12c5\u12c8\u12d6\u12d8\u1310\u1312\u1315\u1318\u135a\u135d\u135f" + + "\u1380\u138f\u13a0\u13f5\u13f8\u13fd\u1401\u166c\u166f\u167f\u1681\u169a\u16a0\u16ea\u16ee\u16f8" + + "\u1700\u1715\u171f\u1734\u1740\u1753\u1760\u176c\u176e\u1770\u1772\u1773\u1780\u17d3\u17d7\u17d7" + + "\u17db\u17dd\u17e0\u17e9\u180b\u1819\u1820\u1878\u1880\u18aa\u18b0\u18f5\u1900\u191e\u1920\u192b" + + "\u1930\u193b\u1946\u196d\u1970\u1974\u1980\u19ab\u19b0\u19c9\u19d0\u19d9\u1a00\u1a1b\u1a20\u1a5e" + + "\u1a60\u1a7c\u1a7f\u1a89\u1a90\u1a99\u1aa7\u1aa7\u1ab0\u1abd\u1abf\u1ace\u1b00\u1b4c\u1b50\u1b59" + + "\u1b6b\u1b73\u1b80\u1bf3\u1c00\u1c37\u1c40\u1c49\u1c4d\u1c7d\u1c80\u1c88\u1c90\u1cba\u1cbd\u1cbf" + + "\u1cd0\u1cd2\u1cd4\u1cfa\u1d00\u1f15\u1f18\u1f1d\u1f20\u1f45\u1f48\u1f4d\u1f50\u1f57\u1f59\u1f59" + + "\u1f5b\u1f5b\u1f5d\u1f5d\u1f5f\u1f7d\u1f80\u1fb4\u1fb6\u1fbc\u1fbe\u1fbe\u1fc2\u1fc4\u1fc6\u1fcc" + + "\u1fd0\u1fd3\u1fd6\u1fdb\u1fe0\u1fec\u1ff2\u1ff4\u1ff6\u1ffc\u200b\u200f\u202a\u202e\u203f\u2040" + + "\u2054\u2054\u2060\u2064\u2066\u206f\u2071\u2071\u207f\u207f\u2090\u209c\u20a0\u20c0\u20d0\u20dc" + + "\u20e1\u20e1\u20e5\u20f0\u2102\u2102\u2107\u2107\u210a\u2113\u2115\u2115\u2119\u211d\u2124\u2124" + + "\u2126\u2126\u2128\u2128\u212a\u212d\u212f\u2139\u213c\u213f\u2145\u2149\u214e\u214e\u2160\u2188" + + "\u2c00\u2ce4\u2ceb\u2cf3\u2d00\u2d25\u2d27\u2d27\u2d2d\u2d2d\u2d30\u2d67\u2d6f\u2d6f\u2d7f\u2d96" + + "\u2da0\u2da6\u2da8\u2dae\u2db0\u2db6\u2db8\u2dbe\u2dc0\u2dc6\u2dc8\u2dce\u2dd0\u2dd6\u2dd8\u2dde" + + "\u2de0\u2dff\u2e2f\u2e2f\u3005\u3007\u3021\u302f\u3031\u3035\u3038\u303c\u3041\u3096\u3099\u309a" + + "\u309d\u309f\u30a1\u30fa\u30fc\u30ff\u3105\u312f\u3131\u318e\u31a0\u31bf\u31f0\u31ff\u3400\u4dbf" + + "\u4e00\ua48c\ua4d0\ua4fd\ua500\ua60c\ua610\ua62b\ua640\ua66f\ua674\ua67d\ua67f\ua6f1\ua717\ua71f" + + "\ua722\ua788\ua78b\ua7ca\ua7d0\ua7d1\ua7d3\ua7d3\ua7d5\ua7d9\ua7f2\ua827\ua82c\ua82c\ua838\ua838" + + "\ua840\ua873\ua880\ua8c5\ua8d0\ua8d9\ua8e0\ua8f7\ua8fb\ua8fb\ua8fd\ua92d\ua930\ua953\ua960\ua97c" + + "\ua980\ua9c0\ua9cf\ua9d9\ua9e0\ua9fe\uaa00\uaa36\uaa40\uaa4d\uaa50\uaa59\uaa60\uaa76\uaa7a\uaac2" + + "\uaadb\uaadd\uaae0\uaaef\uaaf2\uaaf6\uab01\uab06\uab09\uab0e\uab11\uab16\uab20\uab26\uab28\uab2e" + + "\uab30\uab5a\uab5c\uab69\uab70\uabea\uabec\uabed\uabf0\uabf9\uac00\ud7a3\ud7b0\ud7c6\ud7cb\ud7fb" + + "\uf900\ufa6d\ufa70\ufad9\ufb00\ufb06\ufb13\ufb17\ufb1d\ufb28\ufb2a\ufb36\ufb38\ufb3c\ufb3e\ufb3e" + + "\ufb40\ufb41\ufb43\ufb44\ufb46\ufbb1\ufbd3\ufd3d\ufd50\ufd8f\ufd92\ufdc7\ufdf0\ufdfc\ufe00\ufe0f" + + "\ufe20\ufe2f\ufe33\ufe34\ufe4d\ufe4f\ufe69\ufe69\ufe70\ufe74\ufe76\ufefc\ufeff\ufeff\uff04\uff04" + + "\uff10\uff19\uff21\uff3a\uff3f\uff3f\uff41\uff5a\uff66\uffbe\uffc2\uffc7\uffca\uffcf\uffd2\uffd7" + + "\uffda\uffdc\uffe0\uffe1\uffe5\uffe6\ufff9\ufffb"; + + const string SupportedIdentifierPartRanges = + "\u0024\u0024\u0030\u0039\u0041\u005a\u005f\u005f\u0061\u007a\u00a2\u00a5\u00aa\u00aa\u00b5\u00b5" + + "\u00ba\u00ba\u00c0\u00d6\u00d8\u00f6\u00f8\u02c1\u02c6\u02d1\u02e0\u02e4\u02ec\u02ec\u02ee\u02ee" + + "\u0370\u0374\u0376\u0377\u037a\u037d\u037f\u037f\u0386\u0386\u0388\u038a\u038c\u038c\u038e\u03a1" + + "\u03a3\u03f5\u03f7\u0481\u048a\u052f\u0531\u0556\u0559\u0559\u0560\u0588\u058f\u058f\u05d0\u05ea" + + "\u05ef\u05f2\u060b\u060b\u0620\u064a\u0660\u0669\u066e\u066f\u0671\u06d3\u06d5\u06d5\u06e5\u06e6" + + "\u06ee\u06fc\u06ff\u06ff\u0710\u0710\u0712\u072f\u074d\u07a5\u07b1\u07b1\u07c0\u07ea\u07f4\u07f5" + + "\u07fa\u07fa\u07fe\u0815\u081a\u081a\u0824\u0824\u0828\u0828\u0840\u0858\u0860\u086a\u0870\u0887" + + "\u0889\u088e\u08a0\u08c9\u0904\u0939\u093d\u093d\u0950\u0950\u0958\u0961\u0966\u096f\u0971\u0980" + + "\u0985\u098c\u098f\u0990\u0993\u09a8\u09aa\u09b0\u09b2\u09b2\u09b6\u09b9\u09bd\u09bd\u09ce\u09ce" + + "\u09dc\u09dd\u09df\u09e1\u09e6\u09f3\u09fb\u09fc\u0a05\u0a0a\u0a0f\u0a10\u0a13\u0a28\u0a2a\u0a30" + + "\u0a32\u0a33\u0a35\u0a36\u0a38\u0a39\u0a59\u0a5c\u0a5e\u0a5e\u0a66\u0a6f\u0a72\u0a74\u0a85\u0a8d" + + "\u0a8f\u0a91\u0a93\u0aa8\u0aaa\u0ab0\u0ab2\u0ab3\u0ab5\u0ab9\u0abd\u0abd\u0ad0\u0ad0\u0ae0\u0ae1" + + "\u0ae6\u0aef\u0af1\u0af1\u0af9\u0af9\u0b05\u0b0c\u0b0f\u0b10\u0b13\u0b28\u0b2a\u0b30\u0b32\u0b33" + + "\u0b35\u0b39\u0b3d\u0b3d\u0b5c\u0b5d\u0b5f\u0b61\u0b66\u0b6f\u0b71\u0b71\u0b83\u0b83\u0b85\u0b8a" + + "\u0b8e\u0b90\u0b92\u0b95\u0b99\u0b9a\u0b9c\u0b9c\u0b9e\u0b9f\u0ba3\u0ba4\u0ba8\u0baa\u0bae\u0bb9" + + "\u0bd0\u0bd0\u0be6\u0bef\u0bf9\u0bf9\u0c05\u0c0c\u0c0e\u0c10\u0c12\u0c28\u0c2a\u0c39\u0c3d\u0c3d" + + "\u0c58\u0c5a\u0c5d\u0c5d\u0c60\u0c61\u0c66\u0c6f\u0c80\u0c80\u0c85\u0c8c\u0c8e\u0c90\u0c92\u0ca8" + + "\u0caa\u0cb3\u0cb5\u0cb9\u0cbd\u0cbd\u0cdd\u0cde\u0ce0\u0ce1\u0ce6\u0cef\u0cf1\u0cf2\u0d04\u0d0c" + + "\u0d0e\u0d10\u0d12\u0d3a\u0d3d\u0d3d\u0d4e\u0d4e\u0d54\u0d56\u0d5f\u0d61\u0d66\u0d6f\u0d7a\u0d7f" + + "\u0d85\u0d96\u0d9a\u0db1\u0db3\u0dbb\u0dbd\u0dbd\u0dc0\u0dc6\u0de6\u0def\u0e01\u0e30\u0e32\u0e33" + + "\u0e3f\u0e46\u0e50\u0e59\u0e81\u0e82\u0e84\u0e84\u0e86\u0e8a\u0e8c\u0ea3\u0ea5\u0ea5\u0ea7\u0eb0" + + "\u0eb2\u0eb3\u0ebd\u0ebd\u0ec0\u0ec4\u0ec6\u0ec6\u0ed0\u0ed9\u0edc\u0edf\u0f00\u0f00\u0f20\u0f29" + + "\u0f40\u0f47\u0f49\u0f6c\u0f88\u0f8c\u1000\u102a\u103f\u1049\u1050\u1055\u105a\u105d\u1061\u1061" + + "\u1065\u1066\u106e\u1070\u1075\u1081\u108e\u108e\u1090\u1099\u10a0\u10c5\u10c7\u10c7\u10cd\u10cd" + + "\u10d0\u10fa\u10fc\u1248\u124a\u124d\u1250\u1256\u1258\u1258\u125a\u125d\u1260\u1288\u128a\u128d" + + "\u1290\u12b0\u12b2\u12b5\u12b8\u12be\u12c0\u12c0\u12c2\u12c5\u12c8\u12d6\u12d8\u1310\u1312\u1315" + + "\u1318\u135a\u1380\u138f\u13a0\u13f5\u13f8\u13fd\u1401\u166c\u166f\u167f\u1681\u169a\u16a0\u16ea" + + "\u16ee\u16f8\u1700\u1711\u171f\u1731\u1740\u1751\u1760\u176c\u176e\u1770\u1780\u17b3\u17d7\u17d7" + + "\u17db\u17dc\u17e0\u17e9\u1810\u1819\u1820\u1878\u1880\u1884\u1887\u18a8\u18aa\u18aa\u18b0\u18f5" + + "\u1900\u191e\u1946\u196d\u1970\u1974\u1980\u19ab\u19b0\u19c9\u19d0\u19d9\u1a00\u1a16\u1a20\u1a54" + + "\u1a80\u1a89\u1a90\u1a99\u1aa7\u1aa7\u1b05\u1b33\u1b45\u1b4c\u1b50\u1b59\u1b83\u1ba0\u1bae\u1be5" + + "\u1c00\u1c23\u1c40\u1c49\u1c4d\u1c7d\u1c80\u1c88\u1c90\u1cba\u1cbd\u1cbf\u1ce9\u1cec\u1cee\u1cf3" + + "\u1cf5\u1cf6\u1cfa\u1cfa\u1d00\u1dbf\u1e00\u1f15\u1f18\u1f1d\u1f20\u1f45\u1f48\u1f4d\u1f50\u1f57" + + "\u1f59\u1f59\u1f5b\u1f5b\u1f5d\u1f5d\u1f5f\u1f7d\u1f80\u1fb4\u1fb6\u1fbc\u1fbe\u1fbe\u1fc2\u1fc4" + + "\u1fc6\u1fcc\u1fd0\u1fd3\u1fd6\u1fdb\u1fe0\u1fec\u1ff2\u1ff4\u1ff6\u1ffc\u203f\u2040\u2054\u2054" + + "\u2071\u2071\u207f\u207f\u2090\u209c\u20a0\u20c0\u2102\u2102\u2107\u2107\u210a\u2113\u2115\u2115" + + "\u2119\u211d\u2124\u2124\u2126\u2126\u2128\u2128\u212a\u212d\u212f\u2139\u213c\u213f\u2145\u2149" + + "\u214e\u214e\u2160\u2188\u2c00\u2ce4\u2ceb\u2cee\u2cf2\u2cf3\u2d00\u2d25\u2d27\u2d27\u2d2d\u2d2d" + + "\u2d30\u2d67\u2d6f\u2d6f\u2d80\u2d96\u2da0\u2da6\u2da8\u2dae\u2db0\u2db6\u2db8\u2dbe\u2dc0\u2dc6" + + "\u2dc8\u2dce\u2dd0\u2dd6\u2dd8\u2dde\u2e2f\u2e2f\u3005\u3007\u3021\u3029\u3031\u3035\u3038\u303c" + + "\u3041\u3096\u309d\u309f\u30a1\u30fa\u30fc\u30ff\u3105\u312f\u3131\u318e\u31a0\u31bf\u31f0\u31ff" + + "\u3400\u4dbf\u4e00\ua48c\ua4d0\ua4fd\ua500\ua60c\ua610\ua62b\ua640\ua66e\ua67f\ua69d\ua6a0\ua6ef" + + "\ua717\ua71f\ua722\ua788\ua78b\ua7ca\ua7d0\ua7d1\ua7d3\ua7d3\ua7d5\ua7d9\ua7f2\ua801\ua803\ua805" + + "\ua807\ua80a\ua80c\ua822\ua838\ua838\ua840\ua873\ua882\ua8b3\ua8d0\ua8d9\ua8f2\ua8f7\ua8fb\ua8fb" + + "\ua8fd\ua8fe\ua900\ua925\ua930\ua946\ua960\ua97c\ua984\ua9b2\ua9cf\ua9d9\ua9e0\ua9e4\ua9e6\ua9fe" + + "\uaa00\uaa28\uaa40\uaa42\uaa44\uaa4b\uaa50\uaa59\uaa60\uaa76\uaa7a\uaa7a\uaa7e\uaaaf\uaab1\uaab1" + + "\uaab5\uaab6\uaab9\uaabd\uaac0\uaac0\uaac2\uaac2\uaadb\uaadd\uaae0\uaaea\uaaf2\uaaf4\uab01\uab06" + + "\uab09\uab0e\uab11\uab16\uab20\uab26\uab28\uab2e\uab30\uab5a\uab5c\uab69\uab70\uabe2\uabf0\uabf9" + + "\uac00\ud7a3\ud7b0\ud7c6\ud7cb\ud7fb\uf900\ufa6d\ufa70\ufad9\ufb00\ufb06\ufb13\ufb17\ufb1d\ufb1d" + + "\ufb1f\ufb28\ufb2a\ufb36\ufb38\ufb3c\ufb3e\ufb3e\ufb40\ufb41\ufb43\ufb44\ufb46\ufbb1\ufbd3\ufd3d" + + "\ufd50\ufd8f\ufd92\ufdc7\ufdf0\ufdfc\ufe33\ufe34\ufe4d\ufe4f\ufe69\ufe69\ufe70\ufe74\ufe76\ufefc" + + "\uff04\uff04\uff10\uff19\uff21\uff3a\uff3f\uff3f\uff41\uff5a\uff66\uffbe\uffc2\uffc7\uffca\uffcf" + + "\uffd2\uffd7\uffda\uffdc\uffe0\uffe1\uffe5\uffe6"; + + const string SupplementaryIdentifierStartRanges = + "\ud800\udc00\ud800\udc0b\ud800\udc0d\ud800\udc26\ud800\udc28\ud800\udc3a\ud800\udc3c\ud800\udc3d" + + "\ud800\udc3f\ud800\udc4d\ud800\udc50\ud800\udc5d\ud800\udc80\ud800\udcfa\ud800\udd40\ud800\udd74" + + "\ud800\ude80\ud800\ude9c\ud800\udea0\ud800\uded0\ud800\udf00\ud800\udf1f\ud800\udf2d\ud800\udf4a" + + "\ud800\udf50\ud800\udf75\ud800\udf80\ud800\udf9d\ud800\udfa0\ud800\udfc3\ud800\udfc8\ud800\udfcf" + + "\ud800\udfd1\ud800\udfd5\ud801\udc00\ud801\udc9d\ud801\udcb0\ud801\udcd3\ud801\udcd8\ud801\udcfb" + + "\ud801\udd00\ud801\udd27\ud801\udd30\ud801\udd63\ud801\udd70\ud801\udd7a\ud801\udd7c\ud801\udd8a" + + "\ud801\udd8c\ud801\udd92\ud801\udd94\ud801\udd95\ud801\udd97\ud801\udda1\ud801\udda3\ud801\uddb1" + + "\ud801\uddb3\ud801\uddb9\ud801\uddbb\ud801\uddbc\ud801\ude00\ud801\udf36\ud801\udf40\ud801\udf55" + + "\ud801\udf60\ud801\udf67\ud801\udf80\ud801\udf85\ud801\udf87\ud801\udfb0\ud801\udfb2\ud801\udfba" + + "\ud802\udc00\ud802\udc05\ud802\udc08\ud802\udc08\ud802\udc0a\ud802\udc35\ud802\udc37\ud802\udc38" + + "\ud802\udc3c\ud802\udc3c\ud802\udc3f\ud802\udc55\ud802\udc60\ud802\udc76\ud802\udc80\ud802\udc9e" + + "\ud802\udce0\ud802\udcf2\ud802\udcf4\ud802\udcf5\ud802\udd00\ud802\udd15\ud802\udd20\ud802\udd39" + + "\ud802\udd80\ud802\uddb7\ud802\uddbe\ud802\uddbf\ud802\ude00\ud802\ude00\ud802\ude10\ud802\ude13" + + "\ud802\ude15\ud802\ude17\ud802\ude19\ud802\ude35\ud802\ude60\ud802\ude7c\ud802\ude80\ud802\ude9c" + + "\ud802\udec0\ud802\udec7\ud802\udec9\ud802\udee4\ud802\udf00\ud802\udf35\ud802\udf40\ud802\udf55" + + "\ud802\udf60\ud802\udf72\ud802\udf80\ud802\udf91\ud803\udc00\ud803\udc48\ud803\udc80\ud803\udcb2" + + "\ud803\udcc0\ud803\udcf2\ud803\udd00\ud803\udd23\ud803\ude80\ud803\udea9\ud803\udeb0\ud803\udeb1" + + "\ud803\udf00\ud803\udf1c\ud803\udf27\ud803\udf27\ud803\udf30\ud803\udf45\ud803\udf70\ud803\udf81" + + "\ud803\udfb0\ud803\udfc4\ud803\udfe0\ud803\udff6\ud804\udc03\ud804\udc37\ud804\udc71\ud804\udc72" + + "\ud804\udc75\ud804\udc75\ud804\udc83\ud804\udcaf\ud804\udcd0\ud804\udce8\ud804\udd03\ud804\udd26" + + "\ud804\udd44\ud804\udd44\ud804\udd47\ud804\udd47\ud804\udd50\ud804\udd72\ud804\udd76\ud804\udd76" + + "\ud804\udd83\ud804\uddb2\ud804\uddc1\ud804\uddc4\ud804\uddda\ud804\uddda\ud804\udddc\ud804\udddc" + + "\ud804\ude00\ud804\ude11\ud804\ude13\ud804\ude2b\ud804\ude3f\ud804\ude40\ud804\ude80\ud804\ude86" + + "\ud804\ude88\ud804\ude88\ud804\ude8a\ud804\ude8d\ud804\ude8f\ud804\ude9d\ud804\ude9f\ud804\udea8" + + "\ud804\udeb0\ud804\udede\ud804\udf05\ud804\udf0c\ud804\udf0f\ud804\udf10\ud804\udf13\ud804\udf28" + + "\ud804\udf2a\ud804\udf30\ud804\udf32\ud804\udf33\ud804\udf35\ud804\udf39\ud804\udf3d\ud804\udf3d" + + "\ud804\udf50\ud804\udf50\ud804\udf5d\ud804\udf61\ud805\udc00\ud805\udc34\ud805\udc47\ud805\udc4a" + + "\ud805\udc5f\ud805\udc61\ud805\udc80\ud805\udcaf\ud805\udcc4\ud805\udcc5\ud805\udcc7\ud805\udcc7" + + "\ud805\udd80\ud805\uddae\ud805\uddd8\ud805\udddb\ud805\ude00\ud805\ude2f\ud805\ude44\ud805\ude44" + + "\ud805\ude80\ud805\udeaa\ud805\udeb8\ud805\udeb8\ud805\udf00\ud805\udf1a\ud805\udf40\ud805\udf46" + + "\ud806\udc00\ud806\udc2b\ud806\udca0\ud806\udcdf\ud806\udcff\ud806\udd06\ud806\udd09\ud806\udd09" + + "\ud806\udd0c\ud806\udd13\ud806\udd15\ud806\udd16\ud806\udd18\ud806\udd2f\ud806\udd3f\ud806\udd3f" + + "\ud806\udd41\ud806\udd41\ud806\udda0\ud806\udda7\ud806\uddaa\ud806\uddd0\ud806\udde1\ud806\udde1" + + "\ud806\udde3\ud806\udde3\ud806\ude00\ud806\ude00\ud806\ude0b\ud806\ude32\ud806\ude3a\ud806\ude3a" + + "\ud806\ude50\ud806\ude50\ud806\ude5c\ud806\ude89\ud806\ude9d\ud806\ude9d\ud806\udeb0\ud806\udef8" + + "\ud807\udc00\ud807\udc08\ud807\udc0a\ud807\udc2e\ud807\udc40\ud807\udc40\ud807\udc72\ud807\udc8f" + + "\ud807\udd00\ud807\udd06\ud807\udd08\ud807\udd09\ud807\udd0b\ud807\udd30\ud807\udd46\ud807\udd46" + + "\ud807\udd60\ud807\udd65\ud807\udd67\ud807\udd68\ud807\udd6a\ud807\udd89\ud807\udd98\ud807\udd98" + + "\ud807\udee0\ud807\udef2\ud807\udf02\ud807\udf02\ud807\udf04\ud807\udf10\ud807\udf12\ud807\udf33" + + "\ud807\udfb0\ud807\udfb0\ud807\udfdd\ud807\udfe0\ud808\udc00\ud808\udf99\ud809\udc00\ud809\udc6e" + + "\ud809\udc80\ud809\udd43\ud80b\udf90\ud80b\udff0\ud80c\udc00\ud80d\udc2f\ud80d\udc41\ud80d\udc46" + + "\ud811\udc00\ud811\ude46\ud81a\udc00\ud81a\ude38\ud81a\ude40\ud81a\ude5e\ud81a\ude70\ud81a\udebe" + + "\ud81a\uded0\ud81a\udeed\ud81a\udf00\ud81a\udf2f\ud81a\udf40\ud81a\udf43\ud81a\udf63\ud81a\udf77" + + "\ud81a\udf7d\ud81a\udf8f\ud81b\ude40\ud81b\ude7f\ud81b\udf00\ud81b\udf4a\ud81b\udf50\ud81b\udf50" + + "\ud81b\udf93\ud81b\udf9f\ud81b\udfe0\ud81b\udfe1\ud81b\udfe3\ud81b\udfe3\ud81c\udc00\ud821\udff7" + + "\ud822\udc00\ud823\udcd5\ud823\udd00\ud823\udd08\ud82b\udff0\ud82b\udff3\ud82b\udff5\ud82b\udffb" + + "\ud82b\udffd\ud82b\udffe\ud82c\udc00\ud82c\udd22\ud82c\udd32\ud82c\udd32\ud82c\udd50\ud82c\udd52" + + "\ud82c\udd55\ud82c\udd55\ud82c\udd64\ud82c\udd67\ud82c\udd70\ud82c\udefb\ud82f\udc00\ud82f\udc6a" + + "\ud82f\udc70\ud82f\udc7c\ud82f\udc80\ud82f\udc88\ud82f\udc90\ud82f\udc99\ud835\udc00\ud835\udc54" + + "\ud835\udc56\ud835\udc9c\ud835\udc9e\ud835\udc9f\ud835\udca2\ud835\udca2\ud835\udca5\ud835\udca6" + + "\ud835\udca9\ud835\udcac\ud835\udcae\ud835\udcb9\ud835\udcbb\ud835\udcbb\ud835\udcbd\ud835\udcc3" + + "\ud835\udcc5\ud835\udd05\ud835\udd07\ud835\udd0a\ud835\udd0d\ud835\udd14\ud835\udd16\ud835\udd1c" + + "\ud835\udd1e\ud835\udd39\ud835\udd3b\ud835\udd3e\ud835\udd40\ud835\udd44\ud835\udd46\ud835\udd46" + + "\ud835\udd4a\ud835\udd50\ud835\udd52\ud835\udea5\ud835\udea8\ud835\udec0\ud835\udec2\ud835\udeda" + + "\ud835\udedc\ud835\udefa\ud835\udefc\ud835\udf14\ud835\udf16\ud835\udf34\ud835\udf36\ud835\udf4e" + + "\ud835\udf50\ud835\udf6e\ud835\udf70\ud835\udf88\ud835\udf8a\ud835\udfa8\ud835\udfaa\ud835\udfc2" + + "\ud835\udfc4\ud835\udfcb\ud837\udf00\ud837\udf1e\ud837\udf25\ud837\udf2a\ud838\udc30\ud838\udc6d" + + "\ud838\udd00\ud838\udd2c\ud838\udd37\ud838\udd3d\ud838\udd4e\ud838\udd4e\ud838\ude90\ud838\udead" + + "\ud838\udec0\ud838\udeeb\ud838\udeff\ud838\udeff\ud839\udcd0\ud839\udceb\ud839\udfe0\ud839\udfe6" + + "\ud839\udfe8\ud839\udfeb\ud839\udfed\ud839\udfee\ud839\udff0\ud839\udffe\ud83a\udc00\ud83a\udcc4" + + "\ud83a\udd00\ud83a\udd43\ud83a\udd4b\ud83a\udd4b\ud83b\udcb0\ud83b\udcb0\ud83b\ude00\ud83b\ude03" + + "\ud83b\ude05\ud83b\ude1f\ud83b\ude21\ud83b\ude22\ud83b\ude24\ud83b\ude24\ud83b\ude27\ud83b\ude27" + + "\ud83b\ude29\ud83b\ude32\ud83b\ude34\ud83b\ude37\ud83b\ude39\ud83b\ude39\ud83b\ude3b\ud83b\ude3b" + + "\ud83b\ude42\ud83b\ude42\ud83b\ude47\ud83b\ude47\ud83b\ude49\ud83b\ude49\ud83b\ude4b\ud83b\ude4b" + + "\ud83b\ude4d\ud83b\ude4f\ud83b\ude51\ud83b\ude52\ud83b\ude54\ud83b\ude54\ud83b\ude57\ud83b\ude57" + + "\ud83b\ude59\ud83b\ude59\ud83b\ude5b\ud83b\ude5b\ud83b\ude5d\ud83b\ude5d\ud83b\ude5f\ud83b\ude5f" + + "\ud83b\ude61\ud83b\ude62\ud83b\ude64\ud83b\ude64\ud83b\ude67\ud83b\ude6a\ud83b\ude6c\ud83b\ude72" + + "\ud83b\ude74\ud83b\ude77\ud83b\ude79\ud83b\ude7c\ud83b\ude7e\ud83b\ude7e\ud83b\ude80\ud83b\ude89" + + "\ud83b\ude8b\ud83b\ude9b\ud83b\udea1\ud83b\udea3\ud83b\udea5\ud83b\udea9\ud83b\udeab\ud83b\udebb" + + "\ud840\udc00\ud869\udedf\ud869\udf00\ud86d\udf39\ud86d\udf40\ud86e\udc1d\ud86e\udc20\ud873\udea1" + + "\ud873\udeb0\ud87a\udfe0\ud87e\udc00\ud87e\ude1d\ud880\udc00\ud884\udf4a\ud884\udf50\ud888\udfaf"; + + const string SupplementaryIdentifierPartRanges = + "\ud800\udc00\ud800\udc0b\ud800\udc0d\ud800\udc26\ud800\udc28\ud800\udc3a\ud800\udc3c\ud800\udc3d" + + "\ud800\udc3f\ud800\udc4d\ud800\udc50\ud800\udc5d\ud800\udc80\ud800\udcfa\ud800\udd40\ud800\udd74" + + "\ud800\uddfd\ud800\uddfd\ud800\ude80\ud800\ude9c\ud800\udea0\ud800\uded0\ud800\udee0\ud800\udee0" + + "\ud800\udf00\ud800\udf1f\ud800\udf2d\ud800\udf4a\ud800\udf50\ud800\udf7a\ud800\udf80\ud800\udf9d" + + "\ud800\udfa0\ud800\udfc3\ud800\udfc8\ud800\udfcf\ud800\udfd1\ud800\udfd5\ud801\udc00\ud801\udc9d" + + "\ud801\udca0\ud801\udca9\ud801\udcb0\ud801\udcd3\ud801\udcd8\ud801\udcfb\ud801\udd00\ud801\udd27" + + "\ud801\udd30\ud801\udd63\ud801\udd70\ud801\udd7a\ud801\udd7c\ud801\udd8a\ud801\udd8c\ud801\udd92" + + "\ud801\udd94\ud801\udd95\ud801\udd97\ud801\udda1\ud801\udda3\ud801\uddb1\ud801\uddb3\ud801\uddb9" + + "\ud801\uddbb\ud801\uddbc\ud801\ude00\ud801\udf36\ud801\udf40\ud801\udf55\ud801\udf60\ud801\udf67" + + "\ud801\udf80\ud801\udf85\ud801\udf87\ud801\udfb0\ud801\udfb2\ud801\udfba\ud802\udc00\ud802\udc05" + + "\ud802\udc08\ud802\udc08\ud802\udc0a\ud802\udc35\ud802\udc37\ud802\udc38\ud802\udc3c\ud802\udc3c" + + "\ud802\udc3f\ud802\udc55\ud802\udc60\ud802\udc76\ud802\udc80\ud802\udc9e\ud802\udce0\ud802\udcf2" + + "\ud802\udcf4\ud802\udcf5\ud802\udd00\ud802\udd15\ud802\udd20\ud802\udd39\ud802\udd80\ud802\uddb7" + + "\ud802\uddbe\ud802\uddbf\ud802\ude00\ud802\ude03\ud802\ude05\ud802\ude06\ud802\ude0c\ud802\ude13" + + "\ud802\ude15\ud802\ude17\ud802\ude19\ud802\ude35\ud802\ude38\ud802\ude3a\ud802\ude3f\ud802\ude3f" + + "\ud802\ude60\ud802\ude7c\ud802\ude80\ud802\ude9c\ud802\udec0\ud802\udec7\ud802\udec9\ud802\udee6" + + "\ud802\udf00\ud802\udf35\ud802\udf40\ud802\udf55\ud802\udf60\ud802\udf72\ud802\udf80\ud802\udf91" + + "\ud803\udc00\ud803\udc48\ud803\udc80\ud803\udcb2\ud803\udcc0\ud803\udcf2\ud803\udd00\ud803\udd27" + + "\ud803\udd30\ud803\udd39\ud803\ude80\ud803\udea9\ud803\udeab\ud803\udeac\ud803\udeb0\ud803\udeb1" + + "\ud803\udefd\ud803\udf1c\ud803\udf27\ud803\udf27\ud803\udf30\ud803\udf50\ud803\udf70\ud803\udf85" + + "\ud803\udfb0\ud803\udfc4\ud803\udfe0\ud803\udff6\ud804\udc00\ud804\udc46\ud804\udc66\ud804\udc75" + + "\ud804\udc7f\ud804\udcba\ud804\udcbd\ud804\udcbd\ud804\udcc2\ud804\udcc2\ud804\udccd\ud804\udccd" + + "\ud804\udcd0\ud804\udce8\ud804\udcf0\ud804\udcf9\ud804\udd00\ud804\udd34\ud804\udd36\ud804\udd3f" + + "\ud804\udd44\ud804\udd47\ud804\udd50\ud804\udd73\ud804\udd76\ud804\udd76\ud804\udd80\ud804\uddc4" + + "\ud804\uddc9\ud804\uddcc\ud804\uddce\ud804\uddda\ud804\udddc\ud804\udddc\ud804\ude00\ud804\ude11" + + "\ud804\ude13\ud804\ude37\ud804\ude3e\ud804\ude41\ud804\ude80\ud804\ude86\ud804\ude88\ud804\ude88" + + "\ud804\ude8a\ud804\ude8d\ud804\ude8f\ud804\ude9d\ud804\ude9f\ud804\udea8\ud804\udeb0\ud804\udeea" + + "\ud804\udef0\ud804\udef9\ud804\udf00\ud804\udf03\ud804\udf05\ud804\udf0c\ud804\udf0f\ud804\udf10" + + "\ud804\udf13\ud804\udf28\ud804\udf2a\ud804\udf30\ud804\udf32\ud804\udf33\ud804\udf35\ud804\udf39" + + "\ud804\udf3b\ud804\udf44\ud804\udf47\ud804\udf48\ud804\udf4b\ud804\udf4d\ud804\udf50\ud804\udf50" + + "\ud804\udf57\ud804\udf57\ud804\udf5d\ud804\udf63\ud804\udf66\ud804\udf6c\ud804\udf70\ud804\udf74" + + "\ud805\udc00\ud805\udc4a\ud805\udc50\ud805\udc59\ud805\udc5e\ud805\udc61\ud805\udc80\ud805\udcc5" + + "\ud805\udcc7\ud805\udcc7\ud805\udcd0\ud805\udcd9\ud805\udd80\ud805\uddb5\ud805\uddb8\ud805\uddc0" + + "\ud805\uddd8\ud805\udddd\ud805\ude00\ud805\ude40\ud805\ude44\ud805\ude44\ud805\ude50\ud805\ude59" + + "\ud805\ude80\ud805\udeb8\ud805\udec0\ud805\udec9\ud805\udf00\ud805\udf1a\ud805\udf1d\ud805\udf2b" + + "\ud805\udf30\ud805\udf39\ud805\udf40\ud805\udf46\ud806\udc00\ud806\udc3a\ud806\udca0\ud806\udce9" + + "\ud806\udcff\ud806\udd06\ud806\udd09\ud806\udd09\ud806\udd0c\ud806\udd13\ud806\udd15\ud806\udd16" + + "\ud806\udd18\ud806\udd35\ud806\udd37\ud806\udd38\ud806\udd3b\ud806\udd43\ud806\udd50\ud806\udd59" + + "\ud806\udda0\ud806\udda7\ud806\uddaa\ud806\uddd7\ud806\uddda\ud806\udde1\ud806\udde3\ud806\udde4" + + "\ud806\ude00\ud806\ude3e\ud806\ude47\ud806\ude47\ud806\ude50\ud806\ude99\ud806\ude9d\ud806\ude9d" + + "\ud806\udeb0\ud806\udef8\ud807\udc00\ud807\udc08\ud807\udc0a\ud807\udc36\ud807\udc38\ud807\udc40" + + "\ud807\udc50\ud807\udc59\ud807\udc72\ud807\udc8f\ud807\udc92\ud807\udca7\ud807\udca9\ud807\udcb6" + + "\ud807\udd00\ud807\udd06\ud807\udd08\ud807\udd09\ud807\udd0b\ud807\udd36\ud807\udd3a\ud807\udd3a" + + "\ud807\udd3c\ud807\udd3d\ud807\udd3f\ud807\udd47\ud807\udd50\ud807\udd59\ud807\udd60\ud807\udd65" + + "\ud807\udd67\ud807\udd68\ud807\udd6a\ud807\udd8e\ud807\udd90\ud807\udd91\ud807\udd93\ud807\udd98" + + "\ud807\udda0\ud807\udda9\ud807\udee0\ud807\udef6\ud807\udf00\ud807\udf10\ud807\udf12\ud807\udf3a" + + "\ud807\udf3e\ud807\udf42\ud807\udf50\ud807\udf59\ud807\udfb0\ud807\udfb0\ud807\udfdd\ud807\udfe0" + + "\ud808\udc00\ud808\udf99\ud809\udc00\ud809\udc6e\ud809\udc80\ud809\udd43\ud80b\udf90\ud80b\udff0" + + "\ud80c\udc00\ud80d\udc55\ud811\udc00\ud811\ude46\ud81a\udc00\ud81a\ude38\ud81a\ude40\ud81a\ude5e" + + "\ud81a\ude60\ud81a\ude69\ud81a\ude70\ud81a\udebe\ud81a\udec0\ud81a\udec9\ud81a\uded0\ud81a\udeed" + + "\ud81a\udef0\ud81a\udef4\ud81a\udf00\ud81a\udf36\ud81a\udf40\ud81a\udf43\ud81a\udf50\ud81a\udf59" + + "\ud81a\udf63\ud81a\udf77\ud81a\udf7d\ud81a\udf8f\ud81b\ude40\ud81b\ude7f\ud81b\udf00\ud81b\udf4a" + + "\ud81b\udf4f\ud81b\udf87\ud81b\udf8f\ud81b\udf9f\ud81b\udfe0\ud81b\udfe1\ud81b\udfe3\ud81b\udfe4" + + "\ud81b\udff0\ud81b\udff1\ud81c\udc00\ud821\udff7\ud822\udc00\ud823\udcd5\ud823\udd00\ud823\udd08" + + "\ud82b\udff0\ud82b\udff3\ud82b\udff5\ud82b\udffb\ud82b\udffd\ud82b\udffe\ud82c\udc00\ud82c\udd22" + + "\ud82c\udd32\ud82c\udd32\ud82c\udd50\ud82c\udd52\ud82c\udd55\ud82c\udd55\ud82c\udd64\ud82c\udd67" + + "\ud82c\udd70\ud82c\udefb\ud82f\udc00\ud82f\udc6a\ud82f\udc70\ud82f\udc7c\ud82f\udc80\ud82f\udc88" + + "\ud82f\udc90\ud82f\udc99\ud82f\udc9d\ud82f\udc9e\ud82f\udca0\ud82f\udca3\ud833\udf00\ud833\udf2d" + + "\ud833\udf30\ud833\udf46\ud834\udd65\ud834\udd69\ud834\udd6d\ud834\udd82\ud834\udd85\ud834\udd8b" + + "\ud834\uddaa\ud834\uddad\ud834\ude42\ud834\ude44\ud835\udc00\ud835\udc54\ud835\udc56\ud835\udc9c" + + "\ud835\udc9e\ud835\udc9f\ud835\udca2\ud835\udca2\ud835\udca5\ud835\udca6\ud835\udca9\ud835\udcac" + + "\ud835\udcae\ud835\udcb9\ud835\udcbb\ud835\udcbb\ud835\udcbd\ud835\udcc3\ud835\udcc5\ud835\udd05" + + "\ud835\udd07\ud835\udd0a\ud835\udd0d\ud835\udd14\ud835\udd16\ud835\udd1c\ud835\udd1e\ud835\udd39" + + "\ud835\udd3b\ud835\udd3e\ud835\udd40\ud835\udd44\ud835\udd46\ud835\udd46\ud835\udd4a\ud835\udd50" + + "\ud835\udd52\ud835\udea5\ud835\udea8\ud835\udec0\ud835\udec2\ud835\udeda\ud835\udedc\ud835\udefa" + + "\ud835\udefc\ud835\udf14\ud835\udf16\ud835\udf34\ud835\udf36\ud835\udf4e\ud835\udf50\ud835\udf6e" + + "\ud835\udf70\ud835\udf88\ud835\udf8a\ud835\udfa8\ud835\udfaa\ud835\udfc2\ud835\udfc4\ud835\udfcb" + + "\ud835\udfce\ud835\udfff\ud836\ude00\ud836\ude36\ud836\ude3b\ud836\ude6c\ud836\ude75\ud836\ude75" + + "\ud836\ude84\ud836\ude84\ud836\ude9b\ud836\ude9f\ud836\udea1\ud836\udeaf\ud837\udf00\ud837\udf1e" + + "\ud837\udf25\ud837\udf2a\ud838\udc00\ud838\udc06\ud838\udc08\ud838\udc18\ud838\udc1b\ud838\udc21" + + "\ud838\udc23\ud838\udc24\ud838\udc26\ud838\udc2a\ud838\udc30\ud838\udc6d\ud838\udc8f\ud838\udc8f" + + "\ud838\udd00\ud838\udd2c\ud838\udd30\ud838\udd3d\ud838\udd40\ud838\udd49\ud838\udd4e\ud838\udd4e" + + "\ud838\ude90\ud838\udeae\ud838\udec0\ud838\udef9\ud838\udeff\ud838\udeff\ud839\udcd0\ud839\udcf9" + + "\ud839\udfe0\ud839\udfe6\ud839\udfe8\ud839\udfeb\ud839\udfed\ud839\udfee\ud839\udff0\ud839\udffe" + + "\ud83a\udc00\ud83a\udcc4\ud83a\udcd0\ud83a\udcd6\ud83a\udd00\ud83a\udd4b\ud83a\udd50\ud83a\udd59" + + "\ud83b\udcb0\ud83b\udcb0\ud83b\ude00\ud83b\ude03\ud83b\ude05\ud83b\ude1f\ud83b\ude21\ud83b\ude22" + + "\ud83b\ude24\ud83b\ude24\ud83b\ude27\ud83b\ude27\ud83b\ude29\ud83b\ude32\ud83b\ude34\ud83b\ude37" + + "\ud83b\ude39\ud83b\ude39\ud83b\ude3b\ud83b\ude3b\ud83b\ude42\ud83b\ude42\ud83b\ude47\ud83b\ude47" + + "\ud83b\ude49\ud83b\ude49\ud83b\ude4b\ud83b\ude4b\ud83b\ude4d\ud83b\ude4f\ud83b\ude51\ud83b\ude52" + + "\ud83b\ude54\ud83b\ude54\ud83b\ude57\ud83b\ude57\ud83b\ude59\ud83b\ude59\ud83b\ude5b\ud83b\ude5b" + + "\ud83b\ude5d\ud83b\ude5d\ud83b\ude5f\ud83b\ude5f\ud83b\ude61\ud83b\ude62\ud83b\ude64\ud83b\ude64" + + "\ud83b\ude67\ud83b\ude6a\ud83b\ude6c\ud83b\ude72\ud83b\ude74\ud83b\ude77\ud83b\ude79\ud83b\ude7c" + + "\ud83b\ude7e\ud83b\ude7e\ud83b\ude80\ud83b\ude89\ud83b\ude8b\ud83b\ude9b\ud83b\udea1\ud83b\udea3" + + "\ud83b\udea5\ud83b\udea9\ud83b\udeab\ud83b\udebb\ud83e\udff0\ud83e\udff9\ud840\udc00\ud869\udedf" + + "\ud869\udf00\ud86d\udf39\ud86d\udf40\ud86e\udc1d\ud86e\udc20\ud873\udea1\ud873\udeb0\ud87a\udfe0" + + "\ud87e\udc00\ud87e\ude1d\ud880\udc00\ud884\udf4a\ud884\udf50\ud888\udfaf\udb40\udc01\udb40\udc01" + + "\udb40\udc20\udb40\udc7f\udb40\udd00\udb40\uddef"; + + const string SupplementarySupportedIdentifierPartRanges = + "\ud800\udc00\ud800\udc0b\ud800\udc0d\ud800\udc26\ud800\udc28\ud800\udc3a\ud800\udc3c\ud800\udc3d" + + "\ud800\udc3f\ud800\udc4d\ud800\udc50\ud800\udc5d\ud800\udc80\ud800\udcfa\ud800\udd40\ud800\udd74" + + "\ud800\ude80\ud800\ude9c\ud800\udea0\ud800\uded0\ud800\udf00\ud800\udf1f\ud800\udf2d\ud800\udf4a" + + "\ud800\udf50\ud800\udf75\ud800\udf80\ud800\udf9d\ud800\udfa0\ud800\udfc3\ud800\udfc8\ud800\udfcf" + + "\ud800\udfd1\ud800\udfd5\ud801\udc00\ud801\udc9d\ud801\udca0\ud801\udca9\ud801\udcb0\ud801\udcd3" + + "\ud801\udcd8\ud801\udcfb\ud801\udd00\ud801\udd27\ud801\udd30\ud801\udd63\ud801\udd70\ud801\udd7a" + + "\ud801\udd7c\ud801\udd8a\ud801\udd8c\ud801\udd92\ud801\udd94\ud801\udd95\ud801\udd97\ud801\udda1" + + "\ud801\udda3\ud801\uddb1\ud801\uddb3\ud801\uddb9\ud801\uddbb\ud801\uddbc\ud801\ude00\ud801\udf36" + + "\ud801\udf40\ud801\udf55\ud801\udf60\ud801\udf67\ud801\udf80\ud801\udf85\ud801\udf87\ud801\udfb0" + + "\ud801\udfb2\ud801\udfba\ud802\udc00\ud802\udc05\ud802\udc08\ud802\udc08\ud802\udc0a\ud802\udc35" + + "\ud802\udc37\ud802\udc38\ud802\udc3c\ud802\udc3c\ud802\udc3f\ud802\udc55\ud802\udc60\ud802\udc76" + + "\ud802\udc80\ud802\udc9e\ud802\udce0\ud802\udcf2\ud802\udcf4\ud802\udcf5\ud802\udd00\ud802\udd15" + + "\ud802\udd20\ud802\udd39\ud802\udd80\ud802\uddb7\ud802\uddbe\ud802\uddbf\ud802\ude00\ud802\ude00" + + "\ud802\ude10\ud802\ude13\ud802\ude15\ud802\ude17\ud802\ude19\ud802\ude35\ud802\ude60\ud802\ude7c" + + "\ud802\ude80\ud802\ude9c\ud802\udec0\ud802\udec7\ud802\udec9\ud802\udee4\ud802\udf00\ud802\udf35" + + "\ud802\udf40\ud802\udf55\ud802\udf60\ud802\udf72\ud802\udf80\ud802\udf91\ud803\udc00\ud803\udc48" + + "\ud803\udc80\ud803\udcb2\ud803\udcc0\ud803\udcf2\ud803\udd00\ud803\udd23\ud803\udd30\ud803\udd39" + + "\ud803\ude80\ud803\udea9\ud803\udeb0\ud803\udeb1\ud803\udf00\ud803\udf1c\ud803\udf27\ud803\udf27" + + "\ud803\udf30\ud803\udf45\ud803\udf70\ud803\udf81\ud803\udfb0\ud803\udfc4\ud803\udfe0\ud803\udff6" + + "\ud804\udc03\ud804\udc37\ud804\udc66\ud804\udc6f\ud804\udc71\ud804\udc72\ud804\udc75\ud804\udc75" + + "\ud804\udc83\ud804\udcaf\ud804\udcd0\ud804\udce8\ud804\udcf0\ud804\udcf9\ud804\udd03\ud804\udd26" + + "\ud804\udd36\ud804\udd3f\ud804\udd44\ud804\udd44\ud804\udd47\ud804\udd47\ud804\udd50\ud804\udd72" + + "\ud804\udd76\ud804\udd76\ud804\udd83\ud804\uddb2\ud804\uddc1\ud804\uddc4\ud804\uddd0\ud804\uddda" + + "\ud804\udddc\ud804\udddc\ud804\ude00\ud804\ude11\ud804\ude13\ud804\ude2b\ud804\ude3f\ud804\ude40" + + "\ud804\ude80\ud804\ude86\ud804\ude88\ud804\ude88\ud804\ude8a\ud804\ude8d\ud804\ude8f\ud804\ude9d" + + "\ud804\ude9f\ud804\udea8\ud804\udeb0\ud804\udede\ud804\udef0\ud804\udef9\ud804\udf05\ud804\udf0c" + + "\ud804\udf0f\ud804\udf10\ud804\udf13\ud804\udf28\ud804\udf2a\ud804\udf30\ud804\udf32\ud804\udf33" + + "\ud804\udf35\ud804\udf39\ud804\udf3d\ud804\udf3d\ud804\udf50\ud804\udf50\ud804\udf5d\ud804\udf61" + + "\ud805\udc00\ud805\udc34\ud805\udc47\ud805\udc4a\ud805\udc50\ud805\udc59\ud805\udc5f\ud805\udc61" + + "\ud805\udc80\ud805\udcaf\ud805\udcc4\ud805\udcc5\ud805\udcc7\ud805\udcc7\ud805\udcd0\ud805\udcd9" + + "\ud805\udd80\ud805\uddae\ud805\uddd8\ud805\udddb\ud805\ude00\ud805\ude2f\ud805\ude44\ud805\ude44" + + "\ud805\ude50\ud805\ude59\ud805\ude80\ud805\udeaa\ud805\udeb8\ud805\udeb8\ud805\udec0\ud805\udec9" + + "\ud805\udf00\ud805\udf1a\ud805\udf30\ud805\udf39\ud805\udf40\ud805\udf46\ud806\udc00\ud806\udc2b" + + "\ud806\udca0\ud806\udce9\ud806\udcff\ud806\udd06\ud806\udd09\ud806\udd09\ud806\udd0c\ud806\udd13" + + "\ud806\udd15\ud806\udd16\ud806\udd18\ud806\udd2f\ud806\udd3f\ud806\udd3f\ud806\udd41\ud806\udd41" + + "\ud806\udd50\ud806\udd59\ud806\udda0\ud806\udda7\ud806\uddaa\ud806\uddd0\ud806\udde1\ud806\udde1" + + "\ud806\udde3\ud806\udde3\ud806\ude00\ud806\ude00\ud806\ude0b\ud806\ude32\ud806\ude3a\ud806\ude3a" + + "\ud806\ude50\ud806\ude50\ud806\ude5c\ud806\ude89\ud806\ude9d\ud806\ude9d\ud806\udeb0\ud806\udef8" + + "\ud807\udc00\ud807\udc08\ud807\udc0a\ud807\udc2e\ud807\udc40\ud807\udc40\ud807\udc50\ud807\udc59" + + "\ud807\udc72\ud807\udc8f\ud807\udd00\ud807\udd06\ud807\udd08\ud807\udd09\ud807\udd0b\ud807\udd30" + + "\ud807\udd46\ud807\udd46\ud807\udd50\ud807\udd59\ud807\udd60\ud807\udd65\ud807\udd67\ud807\udd68" + + "\ud807\udd6a\ud807\udd89\ud807\udd98\ud807\udd98\ud807\udda0\ud807\udda9\ud807\udee0\ud807\udef2" + + "\ud807\udf02\ud807\udf02\ud807\udf04\ud807\udf10\ud807\udf12\ud807\udf33\ud807\udf50\ud807\udf59" + + "\ud807\udfb0\ud807\udfb0\ud807\udfdd\ud807\udfe0\ud808\udc00\ud808\udf99\ud809\udc00\ud809\udc6e" + + "\ud809\udc80\ud809\udd43\ud80b\udf90\ud80b\udff0\ud80c\udc00\ud80d\udc2f\ud80d\udc41\ud80d\udc46" + + "\ud811\udc00\ud811\ude46\ud81a\udc00\ud81a\ude38\ud81a\ude40\ud81a\ude5e\ud81a\ude60\ud81a\ude69" + + "\ud81a\ude70\ud81a\udebe\ud81a\udec0\ud81a\udec9\ud81a\uded0\ud81a\udeed\ud81a\udf00\ud81a\udf2f" + + "\ud81a\udf40\ud81a\udf43\ud81a\udf50\ud81a\udf59\ud81a\udf63\ud81a\udf77\ud81a\udf7d\ud81a\udf8f" + + "\ud81b\ude40\ud81b\ude7f\ud81b\udf00\ud81b\udf4a\ud81b\udf50\ud81b\udf50\ud81b\udf93\ud81b\udf9f" + + "\ud81b\udfe0\ud81b\udfe1\ud81b\udfe3\ud81b\udfe3\ud81c\udc00\ud821\udff7\ud822\udc00\ud823\udcd5" + + "\ud823\udd00\ud823\udd08\ud82b\udff0\ud82b\udff3\ud82b\udff5\ud82b\udffb\ud82b\udffd\ud82b\udffe" + + "\ud82c\udc00\ud82c\udd22\ud82c\udd32\ud82c\udd32\ud82c\udd50\ud82c\udd52\ud82c\udd55\ud82c\udd55" + + "\ud82c\udd64\ud82c\udd67\ud82c\udd70\ud82c\udefb\ud82f\udc00\ud82f\udc6a\ud82f\udc70\ud82f\udc7c" + + "\ud82f\udc80\ud82f\udc88\ud82f\udc90\ud82f\udc99\ud835\udc00\ud835\udc54\ud835\udc56\ud835\udc9c" + + "\ud835\udc9e\ud835\udc9f\ud835\udca2\ud835\udca2\ud835\udca5\ud835\udca6\ud835\udca9\ud835\udcac" + + "\ud835\udcae\ud835\udcb9\ud835\udcbb\ud835\udcbb\ud835\udcbd\ud835\udcc3\ud835\udcc5\ud835\udd05" + + "\ud835\udd07\ud835\udd0a\ud835\udd0d\ud835\udd14\ud835\udd16\ud835\udd1c\ud835\udd1e\ud835\udd39" + + "\ud835\udd3b\ud835\udd3e\ud835\udd40\ud835\udd44\ud835\udd46\ud835\udd46\ud835\udd4a\ud835\udd50" + + "\ud835\udd52\ud835\udea5\ud835\udea8\ud835\udec0\ud835\udec2\ud835\udeda\ud835\udedc\ud835\udefa" + + "\ud835\udefc\ud835\udf14\ud835\udf16\ud835\udf34\ud835\udf36\ud835\udf4e\ud835\udf50\ud835\udf6e" + + "\ud835\udf70\ud835\udf88\ud835\udf8a\ud835\udfa8\ud835\udfaa\ud835\udfc2\ud835\udfc4\ud835\udfcb" + + "\ud835\udfce\ud835\udfff\ud837\udf00\ud837\udf1e\ud837\udf25\ud837\udf2a\ud838\udc30\ud838\udc6d" + + "\ud838\udd00\ud838\udd2c\ud838\udd37\ud838\udd3d\ud838\udd40\ud838\udd49\ud838\udd4e\ud838\udd4e" + + "\ud838\ude90\ud838\udead\ud838\udec0\ud838\udeeb\ud838\udef0\ud838\udef9\ud838\udeff\ud838\udeff" + + "\ud839\udcd0\ud839\udceb\ud839\udcf0\ud839\udcf9\ud839\udfe0\ud839\udfe6\ud839\udfe8\ud839\udfeb" + + "\ud839\udfed\ud839\udfee\ud839\udff0\ud839\udffe\ud83a\udc00\ud83a\udcc4\ud83a\udd00\ud83a\udd43" + + "\ud83a\udd4b\ud83a\udd4b\ud83a\udd50\ud83a\udd59\ud83b\udcb0\ud83b\udcb0\ud83b\ude00\ud83b\ude03" + + "\ud83b\ude05\ud83b\ude1f\ud83b\ude21\ud83b\ude22\ud83b\ude24\ud83b\ude24\ud83b\ude27\ud83b\ude27" + + "\ud83b\ude29\ud83b\ude32\ud83b\ude34\ud83b\ude37\ud83b\ude39\ud83b\ude39\ud83b\ude3b\ud83b\ude3b" + + "\ud83b\ude42\ud83b\ude42\ud83b\ude47\ud83b\ude47\ud83b\ude49\ud83b\ude49\ud83b\ude4b\ud83b\ude4b" + + "\ud83b\ude4d\ud83b\ude4f\ud83b\ude51\ud83b\ude52\ud83b\ude54\ud83b\ude54\ud83b\ude57\ud83b\ude57" + + "\ud83b\ude59\ud83b\ude59\ud83b\ude5b\ud83b\ude5b\ud83b\ude5d\ud83b\ude5d\ud83b\ude5f\ud83b\ude5f" + + "\ud83b\ude61\ud83b\ude62\ud83b\ude64\ud83b\ude64\ud83b\ude67\ud83b\ude6a\ud83b\ude6c\ud83b\ude72" + + "\ud83b\ude74\ud83b\ude77\ud83b\ude79\ud83b\ude7c\ud83b\ude7e\ud83b\ude7e\ud83b\ude80\ud83b\ude89" + + "\ud83b\ude8b\ud83b\ude9b\ud83b\udea1\ud83b\udea3\ud83b\udea5\ud83b\udea9\ud83b\udeab\ud83b\udebb" + + "\ud83e\udff0\ud83e\udff9\ud840\udc00\ud869\udedf\ud869\udf00\ud86d\udf39\ud86d\udf40\ud86e\udc1d" + + "\ud86e\udc20\ud873\udea1\ud873\udeb0\ud87a\udfe0\ud87e\udc00\ud87e\ude1d\ud880\udc00\ud884\udf4a" + + "\ud884\udf50\ud888\udfaf"; + + const string PackageIdentifierStartRanges = + "\u0024\u0024\u0041\u005a\u005f\u005f\u0061\u007a\u00aa\u00aa\u00b5\u00b5\u00ba\u00ba\u00c0\u00d6" + + "\u00d8\u00f6\u00f8\u02c1\u02c6\u02d1\u02e0\u02e4\u02ec\u02ec\u02ee\u02ee\u0370\u0374\u0376\u0377" + + "\u037a\u037d\u037f\u037f\u0386\u0386\u0388\u038a\u038c\u038c\u038e\u03a1\u03a3\u03f5\u03f7\u0481" + + "\u048a\u052f\u0531\u0556\u0559\u0559\u0560\u0588\u05d0\u05ea\u05ef\u05f2\u0620\u064a\u066e\u066f" + + "\u0671\u06d3\u06d5\u06d5\u06e5\u06e6\u06ee\u06ef\u06fa\u06fc\u06ff\u06ff\u0710\u0710\u0712\u072f" + + "\u074d\u07a5\u07b1\u07b1\u07ca\u07ea\u07f4\u07f5\u07fa\u07fa\u0800\u0815\u081a\u081a\u0824\u0824" + + "\u0828\u0828\u0840\u0858\u0860\u086a\u0870\u0887\u0889\u088e\u08a0\u08c9\u0904\u0939\u093d\u093d" + + "\u0950\u0950\u0958\u0961\u0971\u0980\u0985\u098c\u098f\u0990\u0993\u09a8\u09aa\u09b0\u09b2\u09b2" + + "\u09b6\u09b9\u09bd\u09bd\u09ce\u09ce\u09dc\u09dd\u09df\u09e1\u09f0\u09f1\u09fc\u09fc\u0a05\u0a0a" + + "\u0a0f\u0a10\u0a13\u0a28\u0a2a\u0a30\u0a32\u0a33\u0a35\u0a36\u0a38\u0a39\u0a59\u0a5c\u0a5e\u0a5e" + + "\u0a72\u0a74\u0a85\u0a8d\u0a8f\u0a91\u0a93\u0aa8\u0aaa\u0ab0\u0ab2\u0ab3\u0ab5\u0ab9\u0abd\u0abd" + + "\u0ad0\u0ad0\u0ae0\u0ae1\u0af9\u0af9\u0b05\u0b0c\u0b0f\u0b10\u0b13\u0b28\u0b2a\u0b30\u0b32\u0b33" + + "\u0b35\u0b39\u0b3d\u0b3d\u0b5c\u0b5d\u0b5f\u0b61\u0b71\u0b71\u0b83\u0b83\u0b85\u0b8a\u0b8e\u0b90" + + "\u0b92\u0b95\u0b99\u0b9a\u0b9c\u0b9c\u0b9e\u0b9f\u0ba3\u0ba4\u0ba8\u0baa\u0bae\u0bb9\u0bd0\u0bd0" + + "\u0c05\u0c0c\u0c0e\u0c10\u0c12\u0c28\u0c2a\u0c39\u0c3d\u0c3d\u0c58\u0c5a\u0c5d\u0c5d\u0c60\u0c61" + + "\u0c80\u0c80\u0c85\u0c8c\u0c8e\u0c90\u0c92\u0ca8\u0caa\u0cb3\u0cb5\u0cb9\u0cbd\u0cbd\u0cdd\u0cde" + + "\u0ce0\u0ce1\u0cf1\u0cf2\u0d04\u0d0c\u0d0e\u0d10\u0d12\u0d3a\u0d3d\u0d3d\u0d4e\u0d4e\u0d54\u0d56" + + "\u0d5f\u0d61\u0d7a\u0d7f\u0d85\u0d96\u0d9a\u0db1\u0db3\u0dbb\u0dbd\u0dbd\u0dc0\u0dc6\u0e01\u0e30" + + "\u0e32\u0e33\u0e40\u0e46\u0e81\u0e82\u0e84\u0e84\u0e86\u0e8a\u0e8c\u0ea3\u0ea5\u0ea5\u0ea7\u0eb0" + + "\u0eb2\u0eb3\u0ebd\u0ebd\u0ec0\u0ec4\u0ec6\u0ec6\u0edc\u0edf\u0f00\u0f00\u0f40\u0f47\u0f49\u0f6c" + + "\u0f88\u0f8c\u1000\u102a\u103f\u103f\u1050\u1055\u105a\u105d\u1061\u1061\u1065\u1066\u106e\u1070" + + "\u1075\u1081\u108e\u108e\u10a0\u10c5\u10c7\u10c7\u10cd\u10cd\u10d0\u10fa\u10fc\u1248\u124a\u124d" + + "\u1250\u1256\u1258\u1258\u125a\u125d\u1260\u1288\u128a\u128d\u1290\u12b0\u12b2\u12b5\u12b8\u12be" + + "\u12c0\u12c0\u12c2\u12c5\u12c8\u12d6\u12d8\u1310\u1312\u1315\u1318\u135a\u1380\u138f\u13a0\u13f5" + + "\u13f8\u13fd\u1401\u166c\u166f\u167f\u1681\u169a\u16a0\u16ea\u16ee\u16f8\u1700\u1711\u171f\u1731" + + "\u1740\u1751\u1760\u176c\u176e\u1770\u1780\u17b3\u17d7\u17d7\u17dc\u17dc\u1820\u1878\u1880\u1884" + + "\u1887\u18a8\u18aa\u18aa\u18b0\u18f5\u1900\u191e\u1950\u196d\u1970\u1974\u1980\u19ab\u19b0\u19c9" + + "\u1a00\u1a16\u1a20\u1a54\u1aa7\u1aa7\u1b05\u1b33\u1b45\u1b4c\u1b83\u1ba0\u1bae\u1baf\u1bba\u1be5" + + "\u1c00\u1c23\u1c4d\u1c4f\u1c5a\u1c7d\u1c80\u1c88\u1c90\u1cba\u1cbd\u1cbf\u1ce9\u1cec\u1cee\u1cf3" + + "\u1cf5\u1cf6\u1cfa\u1cfa\u1d00\u1dbf\u1e00\u1f15\u1f18\u1f1d\u1f20\u1f45\u1f48\u1f4d\u1f50\u1f57" + + "\u1f59\u1f59\u1f5b\u1f5b\u1f5d\u1f5d\u1f5f\u1f7d\u1f80\u1fb4\u1fb6\u1fbc\u1fbe\u1fbe\u1fc2\u1fc4" + + "\u1fc6\u1fcc\u1fd0\u1fd3\u1fd6\u1fdb\u1fe0\u1fec\u1ff2\u1ff4\u1ff6\u1ffc\u2071\u2071\u207f\u207f" + + "\u2090\u209c\u2102\u2102\u2107\u2107\u210a\u2113\u2115\u2115\u2119\u211d\u2124\u2124\u2126\u2126" + + "\u2128\u2128\u212a\u212d\u212f\u2139\u213c\u213f\u2145\u2149\u214e\u214e\u2160\u2188\u2c00\u2ce4" + + "\u2ceb\u2cee\u2cf2\u2cf3\u2d00\u2d25\u2d27\u2d27\u2d2d\u2d2d\u2d30\u2d67\u2d6f\u2d6f\u2d80\u2d96" + + "\u2da0\u2da6\u2da8\u2dae\u2db0\u2db6\u2db8\u2dbe\u2dc0\u2dc6\u2dc8\u2dce\u2dd0\u2dd6\u2dd8\u2dde" + + "\u2e2f\u2e2f\u3005\u3007\u3021\u3029\u3031\u3035\u3038\u303c\u3041\u3096\u309d\u309f\u30a1\u30fa" + + "\u30fc\u30ff\u3105\u312f\u3131\u318e\u31a0\u31bf\u31f0\u31ff\u3400\u4dbf\u4e00\ua48c\ua4d0\ua4fd" + + "\ua500\ua60c\ua610\ua61f\ua62a\ua62b\ua640\ua66e\ua67f\ua69d\ua6a0\ua6ef\ua717\ua71f\ua722\ua788" + + "\ua78b\ua7ca\ua7d0\ua7d1\ua7d3\ua7d3\ua7d5\ua7d9\ua7f2\ua801\ua803\ua805\ua807\ua80a\ua80c\ua822" + + "\ua840\ua873\ua882\ua8b3\ua8f2\ua8f7\ua8fb\ua8fb\ua8fd\ua8fe\ua90a\ua925\ua930\ua946\ua960\ua97c" + + "\ua984\ua9b2\ua9cf\ua9cf\ua9e0\ua9e4\ua9e6\ua9ef\ua9fa\ua9fe\uaa00\uaa28\uaa40\uaa42\uaa44\uaa4b" + + "\uaa60\uaa76\uaa7a\uaa7a\uaa7e\uaaaf\uaab1\uaab1\uaab5\uaab6\uaab9\uaabd\uaac0\uaac0\uaac2\uaac2" + + "\uaadb\uaadd\uaae0\uaaea\uaaf2\uaaf4\uab01\uab06\uab09\uab0e\uab11\uab16\uab20\uab26\uab28\uab2e" + + "\uab30\uab5a\uab5c\uab69\uab70\uabe2\uac00\ud7a3\ud7b0\ud7c6\ud7cb\ud7fb\uf900\ufa6d\ufa70\ufad9" + + "\ufb00\ufb06\ufb13\ufb17\ufb1d\ufb1d\ufb1f\ufb28\ufb2a\ufb36\ufb38\ufb3c\ufb3e\ufb3e\ufb40\ufb41" + + "\ufb43\ufb44\ufb46\ufbb1\ufbd3\ufd3d\ufd50\ufd8f\ufd92\ufdc7\ufdf0\ufdfb\ufe70\ufe74\ufe76\ufefc" + + "\uff21\uff3a\uff41\uff5a\uff66\uffbe\uffc2\uffc7\uffca\uffcf\uffd2\uffd7\uffda\uffdc"; + + const string PackageIdentifierPartRanges = + "\u0024\u0024\u0030\u0039\u0041\u005a\u005f\u005f\u0061\u007a\u00aa\u00aa\u00b5\u00b5\u00ba\u00ba" + + "\u00c0\u00d6\u00d8\u00f6\u00f8\u02c1\u02c6\u02d1\u02e0\u02e4\u02ec\u02ec\u02ee\u02ee\u0370\u0374" + + "\u0376\u0377\u037a\u037d\u037f\u037f\u0386\u0386\u0388\u038a\u038c\u038c\u038e\u03a1\u03a3\u03f5" + + "\u03f7\u0481\u048a\u052f\u0531\u0556\u0559\u0559\u0560\u0588\u05d0\u05ea\u05ef\u05f2\u0620\u064a" + + "\u066e\u066f\u0671\u06d3\u06d5\u06d5\u06e5\u06e6\u06ee\u06ef\u06fa\u06fc\u06ff\u06ff\u0710\u0710" + + "\u0712\u072f\u074d\u07a5\u07b1\u07b1\u07ca\u07ea\u07f4\u07f5\u07fa\u07fa\u0800\u0815\u081a\u081a" + + "\u0824\u0824\u0828\u0828\u0840\u0858\u0860\u086a\u0870\u0887\u0889\u088e\u08a0\u08c9\u0904\u0939" + + "\u093d\u093d\u0950\u0950\u0958\u0961\u0971\u0980\u0985\u098c\u098f\u0990\u0993\u09a8\u09aa\u09b0" + + "\u09b2\u09b2\u09b6\u09b9\u09bd\u09bd\u09ce\u09ce\u09dc\u09dd\u09df\u09e1\u09f0\u09f1\u09fc\u09fc" + + "\u0a05\u0a0a\u0a0f\u0a10\u0a13\u0a28\u0a2a\u0a30\u0a32\u0a33\u0a35\u0a36\u0a38\u0a39\u0a59\u0a5c" + + "\u0a5e\u0a5e\u0a72\u0a74\u0a85\u0a8d\u0a8f\u0a91\u0a93\u0aa8\u0aaa\u0ab0\u0ab2\u0ab3\u0ab5\u0ab9" + + "\u0abd\u0abd\u0ad0\u0ad0\u0ae0\u0ae1\u0af9\u0af9\u0b05\u0b0c\u0b0f\u0b10\u0b13\u0b28\u0b2a\u0b30" + + "\u0b32\u0b33\u0b35\u0b39\u0b3d\u0b3d\u0b5c\u0b5d\u0b5f\u0b61\u0b71\u0b71\u0b83\u0b83\u0b85\u0b8a" + + "\u0b8e\u0b90\u0b92\u0b95\u0b99\u0b9a\u0b9c\u0b9c\u0b9e\u0b9f\u0ba3\u0ba4\u0ba8\u0baa\u0bae\u0bb9" + + "\u0bd0\u0bd0\u0c05\u0c0c\u0c0e\u0c10\u0c12\u0c28\u0c2a\u0c39\u0c3d\u0c3d\u0c58\u0c5a\u0c5d\u0c5d" + + "\u0c60\u0c61\u0c80\u0c80\u0c85\u0c8c\u0c8e\u0c90\u0c92\u0ca8\u0caa\u0cb3\u0cb5\u0cb9\u0cbd\u0cbd" + + "\u0cdd\u0cde\u0ce0\u0ce1\u0cf1\u0cf2\u0d04\u0d0c\u0d0e\u0d10\u0d12\u0d3a\u0d3d\u0d3d\u0d4e\u0d4e" + + "\u0d54\u0d56\u0d5f\u0d61\u0d7a\u0d7f\u0d85\u0d96\u0d9a\u0db1\u0db3\u0dbb\u0dbd\u0dbd\u0dc0\u0dc6" + + "\u0e01\u0e30\u0e32\u0e33\u0e40\u0e46\u0e81\u0e82\u0e84\u0e84\u0e86\u0e8a\u0e8c\u0ea3\u0ea5\u0ea5" + + "\u0ea7\u0eb0\u0eb2\u0eb3\u0ebd\u0ebd\u0ec0\u0ec4\u0ec6\u0ec6\u0edc\u0edf\u0f00\u0f00\u0f40\u0f47" + + "\u0f49\u0f6c\u0f88\u0f8c\u1000\u102a\u103f\u103f\u1050\u1055\u105a\u105d\u1061\u1061\u1065\u1066" + + "\u106e\u1070\u1075\u1081\u108e\u108e\u10a0\u10c5\u10c7\u10c7\u10cd\u10cd\u10d0\u10fa\u10fc\u1248" + + "\u124a\u124d\u1250\u1256\u1258\u1258\u125a\u125d\u1260\u1288\u128a\u128d\u1290\u12b0\u12b2\u12b5" + + "\u12b8\u12be\u12c0\u12c0\u12c2\u12c5\u12c8\u12d6\u12d8\u1310\u1312\u1315\u1318\u135a\u1380\u138f" + + "\u13a0\u13f5\u13f8\u13fd\u1401\u166c\u166f\u167f\u1681\u169a\u16a0\u16ea\u16ee\u16f8\u1700\u1711" + + "\u171f\u1731\u1740\u1751\u1760\u176c\u176e\u1770\u1780\u17b3\u17d7\u17d7\u17dc\u17dc\u1820\u1878" + + "\u1880\u1884\u1887\u18a8\u18aa\u18aa\u18b0\u18f5\u1900\u191e\u1950\u196d\u1970\u1974\u1980\u19ab" + + "\u19b0\u19c9\u1a00\u1a16\u1a20\u1a54\u1aa7\u1aa7\u1b05\u1b33\u1b45\u1b4c\u1b83\u1ba0\u1bae\u1baf" + + "\u1bba\u1be5\u1c00\u1c23\u1c4d\u1c4f\u1c5a\u1c7d\u1c80\u1c88\u1c90\u1cba\u1cbd\u1cbf\u1ce9\u1cec" + + "\u1cee\u1cf3\u1cf5\u1cf6\u1cfa\u1cfa\u1d00\u1dbf\u1e00\u1f15\u1f18\u1f1d\u1f20\u1f45\u1f48\u1f4d" + + "\u1f50\u1f57\u1f59\u1f59\u1f5b\u1f5b\u1f5d\u1f5d\u1f5f\u1f7d\u1f80\u1fb4\u1fb6\u1fbc\u1fbe\u1fbe" + + "\u1fc2\u1fc4\u1fc6\u1fcc\u1fd0\u1fd3\u1fd6\u1fdb\u1fe0\u1fec\u1ff2\u1ff4\u1ff6\u1ffc\u2071\u2071" + + "\u207f\u207f\u2090\u209c\u2102\u2102\u2107\u2107\u210a\u2113\u2115\u2115\u2119\u211d\u2124\u2124" + + "\u2126\u2126\u2128\u2128\u212a\u212d\u212f\u2139\u213c\u213f\u2145\u2149\u214e\u214e\u2160\u2188" + + "\u2c00\u2ce4\u2ceb\u2cee\u2cf2\u2cf3\u2d00\u2d25\u2d27\u2d27\u2d2d\u2d2d\u2d30\u2d67\u2d6f\u2d6f" + + "\u2d80\u2d96\u2da0\u2da6\u2da8\u2dae\u2db0\u2db6\u2db8\u2dbe\u2dc0\u2dc6\u2dc8\u2dce\u2dd0\u2dd6" + + "\u2dd8\u2dde\u2e2f\u2e2f\u3005\u3007\u3021\u3029\u3031\u3035\u3038\u303c\u3041\u3096\u309d\u309f" + + "\u30a1\u30fa\u30fc\u30ff\u3105\u312f\u3131\u318e\u31a0\u31bf\u31f0\u31ff\u3400\u4dbf\u4e00\ua48c" + + "\ua4d0\ua4fd\ua500\ua60c\ua610\ua61f\ua62a\ua62b\ua640\ua66e\ua67f\ua69d\ua6a0\ua6ef\ua717\ua71f" + + "\ua722\ua788\ua78b\ua7ca\ua7d0\ua7d1\ua7d3\ua7d3\ua7d5\ua7d9\ua7f2\ua801\ua803\ua805\ua807\ua80a" + + "\ua80c\ua822\ua840\ua873\ua882\ua8b3\ua8f2\ua8f7\ua8fb\ua8fb\ua8fd\ua8fe\ua90a\ua925\ua930\ua946" + + "\ua960\ua97c\ua984\ua9b2\ua9cf\ua9cf\ua9e0\ua9e4\ua9e6\ua9ef\ua9fa\ua9fe\uaa00\uaa28\uaa40\uaa42" + + "\uaa44\uaa4b\uaa60\uaa76\uaa7a\uaa7a\uaa7e\uaaaf\uaab1\uaab1\uaab5\uaab6\uaab9\uaabd\uaac0\uaac0" + + "\uaac2\uaac2\uaadb\uaadd\uaae0\uaaea\uaaf2\uaaf4\uab01\uab06\uab09\uab0e\uab11\uab16\uab20\uab26" + + "\uab28\uab2e\uab30\uab5a\uab5c\uab69\uab70\uabe2\uac00\ud7a3\ud7b0\ud7c6\ud7cb\ud7fb\uf900\ufa6d" + + "\ufa70\ufad9\ufb00\ufb06\ufb13\ufb17\ufb1d\ufb1d\ufb1f\ufb28\ufb2a\ufb36\ufb38\ufb3c\ufb3e\ufb3e" + + "\ufb40\ufb41\ufb43\ufb44\ufb46\ufbb1\ufbd3\ufd3d\ufd50\ufd8f\ufd92\ufdc7\ufdf0\ufdfb\ufe70\ufe74" + + "\ufe76\ufefc\uff21\uff3a\uff41\uff5a\uff66\uffbe\uffc2\uffc7\uffca\uffcf\uffd2\uffd7\uffda\uffdc"; + + internal static bool IsIdentifierStart (char value) => IsInRanges (value, IdentifierStartRanges); + + internal static bool IsIdentifierPart (char value) => IsInRanges (value, IdentifierPartRanges); + + internal static bool IsSupportedIdentifierPart (char value) => IsInRanges (value, SupportedIdentifierPartRanges); + + internal static bool IsIdentifierStart (int value) => + value <= char.MaxValue + ? IsIdentifierStart ((char) value) + : IsInSupplementaryRanges (value, SupplementaryIdentifierStartRanges); + + internal static bool IsIdentifierPart (int value) => + value <= char.MaxValue + ? IsIdentifierPart ((char) value) + : IsInSupplementaryRanges (value, SupplementaryIdentifierPartRanges); + + internal static bool IsSupportedIdentifierPart (int value) => + value <= char.MaxValue + ? IsSupportedIdentifierPart ((char) value) + : IsInSupplementaryRanges (value, SupplementarySupportedIdentifierPartRanges); + + internal static bool IsPackageIdentifierStart (char value) => IsInRanges (value, PackageIdentifierStartRanges); + + internal static bool IsPackageIdentifierPart (char value) => IsInRanges (value, PackageIdentifierPartRanges); + + static bool IsInRanges (char value, string ranges) + { + int low = 0; + int high = ranges.Length / 2 - 1; + while (low <= high) { + int range = low + (high - low) / 2; + char start = ranges [range * 2]; + char end = ranges [range * 2 + 1]; + if (value < start) { + high = range - 1; + } else if (value > end) { + low = range + 1; + } else { + return true; + } + } + return false; + } + + static bool IsInSupplementaryRanges (int value, string ranges) + { + int low = 0; + int high = ranges.Length / 4 - 1; + while (low <= high) { + int range = low + (high - low) / 2; + int offset = range * 4; + int start = char.ConvertToUtf32 (ranges [offset], ranges [offset + 1]); + int end = char.ConvertToUtf32 (ranges [offset + 2], ranges [offset + 3]); + if (value < start) { + high = range - 1; + } else if (value > end) { + low = range + 1; + } else { + return true; + } + } + return false; + } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/JavaNameValidator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/JavaNameValidator.cs index fe5f2933c9c..8ec0ebe3e8b 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/JavaNameValidator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/JavaNameValidator.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Text; namespace Microsoft.Android.Sdk.TrimmableTypeMap; @@ -33,12 +34,64 @@ internal static class JavaNameValidator }; internal static bool IsInvalidIdentifier (string identifier, bool isTypeName) => - JavaKeywords.Contains (identifier) || isTypeName && RestrictedTypeIdentifiers.Contains (identifier); + !IsSupportedIdentifier (identifier, isPackageSegment: false) || + JavaKeywords.Contains (identifier) || + isTypeName && RestrictedTypeIdentifiers.Contains (identifier); + + static bool IsInvalidPackageIdentifier (string identifier) => + !IsSupportedIdentifier (identifier, isPackageSegment: true) || + JavaKeywords.Contains (identifier); + + // JLS 3.8 permits combining, format, and supplementary characters, but generated JCWs also need + // stable source paths and class names throughout javac, AAPT, DEX, and the Android runtime. + static bool IsSupportedIdentifier (string identifier, bool isPackageSegment) + { + if (identifier.Length == 0) { + return false; + } + + int codePointIndex = 0; + for (int i = 0; i < identifier.Length; codePointIndex++) { + char first = identifier [i]; + int value; + if (char.IsHighSurrogate (first)) { + if (i + 1 >= identifier.Length || !char.IsLowSurrogate (identifier [i + 1])) { + return false; + } + // JDK 21 and DEX preserve supplementary identifier characters, but Android's + // class loader cannot resolve classes whose simple name contains them. + return false; + } else if (char.IsLowSurrogate (first)) { + return false; + } else { + value = first; + i++; + } + bool valid = codePointIndex == 0 + ? IsIdentifierStart (value, isPackageSegment) + : IsIdentifierPart (value, isPackageSegment); + if (!valid) { + return false; + } + } + + return identifier.IsNormalized (NormalizationForm.FormC); + } + + static bool IsIdentifierStart (int value, bool isPackageSegment) => + isPackageSegment + ? value <= char.MaxValue && JavaIdentifierData.IsPackageIdentifierStart ((char) value) + : JavaIdentifierData.IsIdentifierStart (value); + + static bool IsIdentifierPart (int value, bool isPackageSegment) => + isPackageSegment + ? value <= char.MaxValue && JavaIdentifierData.IsPackageIdentifierPart ((char) value) + : JavaIdentifierData.IsSupportedIdentifierPart (value); internal static bool TryGetInvalidPackageSegment (string packageName, char separator, out string invalidSegment) { foreach (var segment in packageName.Split (separator)) { - if (JavaKeywords.Contains (segment)) { + if (IsInvalidPackageIdentifier (segment)) { invalidSegment = segment; return true; } @@ -52,7 +105,7 @@ internal static bool TryGetInvalidJniNameSegment (string jniName, out string inv { var segments = jniName.Split ('/'); for (int i = 0; i < segments.Length - 1; i++) { - if (JavaKeywords.Contains (segments [i])) { + if (IsInvalidPackageIdentifier (segments [i])) { invalidSegment = segments [i]; return true; } @@ -68,6 +121,22 @@ internal static bool TryGetInvalidJniNameSegment (string jniName, out string inv return false; } + internal static bool TryGetInvalidJniManifestNameSegment (string jniName, out string invalidSegment) + { + var segments = jniName.Split ('/'); + for (int i = 0; i < segments.Length; i++) { + bool isTypeName = i == segments.Length - 1; + if (IsInvalidPackageIdentifier (segments [i]) || + isTypeName && RestrictedTypeIdentifiers.Contains (segments [i])) { + invalidSegment = segments [i]; + return true; + } + } + + invalidSegment = ""; + return false; + } + internal static bool TryGetInvalidJniSourceTypeSegment (string jniName, out string invalidSegment) { if (TryGetInvalidJniNameSegment (jniName, out invalidSegment)) { @@ -115,9 +184,28 @@ internal static bool TryGetInvalidJavaSourceTypeSegment (string javaType, out st } var segments = typeName.Split ('.'); + for (int i = 0; i < segments.Length; i++) { + var nestedSegments = segments [i].Split ('$'); + for (int nestedIndex = 0; nestedIndex < nestedSegments.Length; nestedIndex++) { + bool isTypeName = i == segments.Length - 1 || nestedIndex > 0; + if (IsInvalidIdentifier (nestedSegments [nestedIndex], isTypeName)) { + invalidSegment = nestedSegments [nestedIndex]; + return true; + } + } + } + + invalidSegment = ""; + return false; + } + + internal static bool TryGetInvalidJavaManifestTypeSegment (string javaType, out string invalidSegment) + { + var segments = javaType.Split ('.'); for (int i = 0; i < segments.Length; i++) { bool isTypeName = i == segments.Length - 1; - if (IsInvalidIdentifier (segments [i], isTypeName)) { + if (IsInvalidPackageIdentifier (segments [i]) || + isTypeName && RestrictedTypeIdentifiers.Contains (segments [i])) { invalidSegment = segments [i]; return true; } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index 86915bff704..2ec3d85dd10 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -395,6 +395,12 @@ public sealed record JavaFieldInfo /// public required string JavaTypeName { get; init; } + /// + /// JNI type descriptor retained for validation where Java source dots cannot distinguish + /// package segments from nested type segments. + /// + internal string? JniTypeName { get; init; } + /// /// Name of the method that initializes this field, e.g., "GetInstance". /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 989afeda295..e7ea21e09e1 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -2638,6 +2638,7 @@ void CollectExportField (MethodDefinition methodDef, AssemblyIndex index, List peers, string? applicationJavaClass = null) + internal bool ValidateJavaNames ( + IReadOnlyList peers, + string? applicationJavaClass = null, + XDocument? manifest = null) { bool valid = true; var reportedNames = new HashSet (StringComparer.Ordinal); if (applicationJavaClass is not null && - JavaNameValidator.TryGetInvalidJavaSourceTypeSegment (applicationJavaClass, out var invalidApplicationIdentifier)) { + JavaNameValidator.TryGetInvalidJavaManifestTypeSegment (applicationJavaClass, out var invalidApplicationIdentifier)) { ReportInvalidName (applicationJavaClass, invalidApplicationIdentifier); } + ValidateManifestComponentNames (); foreach (var peer in peers) { if (!ShouldGenerateJcw (peer)) { continue; } ReportInvalidJniName (peer.JavaName); + if (!peer.IsAbstract && peer.ComponentAttribute is not null && + JavaNameValidator.TryGetInvalidJniManifestNameSegment (peer.JavaName, out var invalidManifestIdentifier)) { + ReportInvalidName (peer.JavaName, invalidManifestIdentifier); + } if (peer.CannotRegisterInStaticConstructor && JavaNameValidator.TryGetInvalidJniSourceTypeSegment (peer.JavaName, out var invalidIdentifier)) { ReportInvalidName (peer.JavaName, invalidIdentifier); @@ -110,8 +121,7 @@ internal bool ValidateJavaNames (IReadOnlyList peers, string? appl ValidateJniSignature (constructor.JniSignature); if (constructor.ThrownNames is not null) { foreach (var thrownName in constructor.ThrownNames) { - var javaThrownName = JniSignatureHelper.JniNameToJavaName (thrownName); - if (JavaNameValidator.TryGetInvalidJavaSourceTypeSegment (javaThrownName, out invalidIdentifier)) { + if (TryGetInvalidThrownNameSegment (thrownName, out invalidIdentifier)) { ReportInvalidName (thrownName, invalidIdentifier); } } @@ -123,15 +133,17 @@ internal bool ValidateJavaNames (IReadOnlyList peers, string? appl } if (method.ThrownNames is not null) { foreach (var thrownName in method.ThrownNames) { - var javaThrownName = JniSignatureHelper.JniNameToJavaName (thrownName); - if (JavaNameValidator.TryGetInvalidJavaSourceTypeSegment (javaThrownName, out invalidIdentifier)) { + if (TryGetInvalidThrownNameSegment (thrownName, out invalidIdentifier)) { ReportInvalidName (thrownName, invalidIdentifier); } } } } foreach (var field in peer.JavaFields) { - if (JavaNameValidator.TryGetInvalidJavaSourceTypeSegment (field.JavaTypeName, out invalidIdentifier)) { + bool invalid = field.JniTypeName is not null + ? JavaNameValidator.TryGetInvalidJniTypeSegment (field.JniTypeName, out _, out invalidIdentifier) + : JavaNameValidator.TryGetInvalidJavaSourceTypeSegment (field.JavaTypeName, out invalidIdentifier); + if (invalid) { ReportInvalidName (field.JavaTypeName, invalidIdentifier); } } @@ -146,6 +158,42 @@ void ValidateJniSignature (string jniSignature) ReportInvalidJniType (JniSignatureHelper.ParseReturnTypeString (jniSignature)); } + static bool TryGetInvalidThrownNameSegment (string thrownName, out string invalidIdentifier) => + thrownName.IndexOf ('/') >= 0 + ? JavaNameValidator.TryGetInvalidJniSourceTypeSegment (thrownName, out invalidIdentifier) + : JavaNameValidator.TryGetInvalidJavaSourceTypeSegment (thrownName, out invalidIdentifier); + + void ValidateManifestComponentNames () + { + if (manifest?.Root is not { } root) { + return; + } + + XNamespace androidNs = "http://schemas.android.com/apk/res/android"; + XName attName = androidNs + "name"; + string packageName = (string?) root.Attribute ("package") ?? ""; + foreach (var element in root.Descendants ()) { + if (!ManifestConstants.ComponentElementNames.Contains (element.Name.LocalName)) { + continue; + } + + ValidateComponentName ((string?) element.Attribute (attName)); + if (element.Name.LocalName == "activity-alias") { + ValidateComponentName ((string?) element.Attribute (androidNs + "targetActivity")); + } + } + + void ValidateComponentName (string? name) + { + if (name is not null) { + string resolvedName = ManifestNameResolver.Resolve (name, packageName); + if (JavaNameValidator.TryGetInvalidJavaManifestTypeSegment (resolvedName, out var invalidIdentifier)) { + ReportInvalidName (resolvedName, invalidIdentifier); + } + } + } + } + void ReportInvalidJniName (string jniName) { if (JavaNameValidator.TryGetInvalidJniNameSegment (jniName, out var invalidIdentifier)) { @@ -446,6 +494,12 @@ internal void RootManifestReferencedTypes (List allPeers, XDocumen } } break; + case "activity-alias": + var targetActivity = (string?) element.Attribute (androidNs + "targetActivity"); + if (targetActivity is not null) { + componentNames.Add (ManifestNameResolver.Resolve (targetActivity, packageName)); + } + break; } } @@ -591,11 +645,8 @@ static void AddPeerByDotName (Dictionary> peersByDotN return doc; } - if (((string?) root.Attribute ("package")).IsNullOrEmpty () && !manifestConfig.PackageName.IsNullOrEmpty ()) { - root.SetAttributeValue ("package", manifestConfig.PackageName); - } - - ManifestGenerator.ApplyPlaceholders (doc, manifestConfig.ManifestPlaceholders); + ManifestGenerator.ResolvePackageName (root, manifestConfig.PackageName); + ManifestGenerator.ApplyPlaceholders (doc, manifestConfig.ManifestPlaceholders, manifestConfig.PackageName); if (!manifestConfig.ApplicationJavaClass.IsNullOrEmpty ()) { var app = root.Element ("application"); diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs index 15af76356e0..32a5c9c372d 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs @@ -1651,7 +1651,7 @@ public static string XA4257 { } /// - /// Looks up a localized string similar to Java name '{0}' contains reserved Java identifier '{1}'. Change the package or type name.. + /// Looks up a localized string similar to Java name '{0}' contains invalid or unsupported Java identifier '{1}'. Change the package or type name.. /// public static string XA4258 { get { diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx index 75a82d67971..7abca19d5cf 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx @@ -1234,10 +1234,10 @@ To use a custom JDK path for a command line build, set the 'JavaSdkDirectory' MS {4} - Full path to the resolved assembly file that was expected to contain the unresolved type - Java name '{0}' contains reserved Java identifier '{1}'. Change the package or type name. + Java name '{0}' contains invalid or unsupported Java identifier '{1}'. Change the package or type name. The following are literal names and should not be translated: Java. {0} - Java package or type name -{1} - Java reserved keyword or restricted identifier +{1} - Invalid, unsupported, reserved, or restricted Java identifier Command '{0}' failed.\n{1} diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index b5512e330fe..ace9a735bba 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -3,8 +3,10 @@ using System.IO; using System.Linq; using System.Security.Cryptography; +using System.Text; using System.Text.Json; using System.Text.RegularExpressions; +using System.Xml.Linq; using NUnit.Framework; using Xamarin.Android.AssemblyStore; using Xamarin.Android.Tasks; @@ -36,6 +38,360 @@ public void Build_WithTrimmableTypeMap_Succeeds ([Values] bool isRelease, [Value AssertTrimmableTypeMapOutputs (intermediateDir); } + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "APT2008", false)] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "XA4258", true)] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "XA4258", true)] + public void Build_JavaTypeOnlyIdentifierStarts_MatchManifestLimitation ( + string typeMapImplementation, + AndroidRuntime runtime, + string expectedCode, + bool expectNoOutputs) + { + bool isRelease = runtime == AndroidRuntime.NativeAOT; + if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { + return; + } + + var proj = new XamarinAndroidApplicationProject { + IsRelease = isRelease, + }; + proj.SetRuntime (runtime); + proj.SetProperty ("AndroidTypeMapImplementation", typeMapImplementation); + proj.Sources.Add (new BuildItem.Source ("ManifestIdentifierStarts.cs") { + TextContent = () => """ + using Android.App; + using Android.Runtime; + + namespace UnnamedProject; + + [Register ("com/example/\u00a2Peer")] + [Activity] + public class CurrencyComponent : Activity + { + public CurrencyComponent () { } + } + + [Register ("com/example/\u203fPeer")] + [Activity] + public class ConnectorComponent : Activity + { + public ConnectorComponent () { } + } + + [Register ("com/example/\U00010428Peer")] + [Activity] + public class SupplementaryComponent : Activity + { + public SupplementaryComponent () { } + } + """, + }); + + using var builder = CreateApkBuilder (); + builder.ThrowOnBuildFailure = false; + Assert.IsFalse ( + builder.Build (proj), + $"{runtime}/{typeMapImplementation} should reject Java-only starts in manifest component names."); + StringAssertEx.Contains ($"error {expectedCode}", builder.LastBuildOutput); + + if (expectNoOutputs) { + StringAssertEx.Contains ("\u00a2Peer", builder.LastBuildOutput); + StringAssertEx.Contains ("\u203fPeer", builder.LastBuildOutput); + StringAssertEx.Contains ("\U00010428Peer", builder.LastBuildOutput); + AssertNoUnicodeOutputs (builder, "\u00a2Peer"); + AssertNoUnicodeOutputs (builder, "\u203fPeer"); + AssertNoUnicodeOutputs (builder, "\U00010428Peer"); + return; + } + + foreach (var javaName in new [] { + "com/example/\u00a2Peer", + "com/example/\u203fPeer", + "com/example/\U00010428Peer", + }) { + var relativePath = (javaName + ".java").Replace ('/', Path.DirectorySeparatorChar); + FileAssert.Exists ( + builder.Output.GetIntermediaryPath (Path.Combine ("android", "src", relativePath)), + $"llvm-ir should generate '{javaName}.java' before AAPT rejects the manifest name."); + } + } + + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, true)] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, false)] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, false)] + public void Build_SupplementaryJavaIdentifier_MatchesRuntimeLimitation ( + string typeMapImplementation, + AndroidRuntime runtime, + bool shouldSucceed) + { + const string javaName = "com/example/\U00010428Peer\U00010400"; + bool isRelease = runtime == AndroidRuntime.NativeAOT; + if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { + return; + } + + var proj = new XamarinAndroidApplicationProject { + IsRelease = isRelease, + }; + proj.SetRuntime (runtime); + proj.SetProperty ("AndroidTypeMapImplementation", typeMapImplementation); + proj.Sources.Add (new BuildItem.Source ("SupplementaryJavaIdentifier.cs") { + TextContent = () => """ + using Android.Runtime; + + namespace UnnamedProject; + + [Register ("com/example/\U00010428Peer\U00010400")] + public class SupplementaryJavaIdentifier : Java.Lang.Object + { + public SupplementaryJavaIdentifier () { } + } + """, + }); + proj.MainActivity = proj.DefaultMainActivity.Replace ( + "//${AFTER_ONCREATE}", + "System.GC.KeepAlive (new UnnamedProject.SupplementaryJavaIdentifier ());"); + + using var builder = CreateApkBuilder (); + builder.ThrowOnBuildFailure = false; + Assert.AreEqual ( + shouldSucceed, + builder.Build (proj), + $"{runtime}/{typeMapImplementation} should match the supplementary runtime limitation."); + if (!shouldSucceed) { + StringAssertEx.Contains ("error XA4258", builder.LastBuildOutput); + StringAssertEx.Contains ("\U00010428Peer\U00010400", builder.LastBuildOutput); + AssertNoUnicodeOutputs (builder, "\U00010428Peer\U00010400"); + return; + } + + var relativeJavaPath = (javaName + ".java").Replace ('/', Path.DirectorySeparatorChar); + var javaPath = new [] { + builder.Output.GetIntermediaryPath (Path.Combine ("android", "src", relativeJavaPath)), + builder.Output.GetIntermediaryPath (Path.Combine ("typemap", "java", relativeJavaPath)), + builder.Output.GetIntermediaryPath (Path.Combine ("typemap", "linked-java", relativeJavaPath)), + }.FirstOrDefault (File.Exists); + Assert.IsNotNull (javaPath, $"{runtime}/{typeMapImplementation} should preserve the supplementary Java source path."); + StringAssert.Contains ("public class \U00010428Peer\U00010400", File.ReadAllText (javaPath)); + + var classFile = builder.Output.GetIntermediaryPath ( + Path.Combine ("android", "bin", "classes", relativeJavaPath.Replace (".java", ".class"))); + FileAssert.Exists (classFile, "javac should accept the supplementary Java identifier."); + + var acwMap = builder.Output.GetIntermediaryPath ("acw-map.txt"); + StringAssert.Contains ( + "UnnamedProject.SupplementaryJavaIdentifier;com.example.\U00010428Peer\U00010400", + File.ReadAllText (acwMap)); + + var dexFile = builder.Output.GetIntermediaryPath (Path.Combine ("android", "bin", "classes.dex")); + Assert.IsTrue ( + DexUtils.ContainsClass ($"L{javaName};", dexFile, AndroidSdkPath), + "llvm-ir should preserve the exact supplementary descriptor even though Android cannot load it."); + } + + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "JAVAC0000", false)] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "XA4258", true)] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "XA4258", true)] + public void Build_InvalidOrUnsupportedUnicodeIdentifiers_ReportDiagnosticWithoutTrimmableOutputs ( + string typeMapImplementation, + AndroidRuntime runtime, + string expectedCode, + bool expectNoOutputs) + { + bool isRelease = runtime == AndroidRuntime.NativeAOT; + if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { + return; + } + + var proj = new XamarinAndroidApplicationProject { + IsRelease = isRelease, + }; + proj.SetRuntime (runtime); + proj.SetProperty ("AndroidTypeMapImplementation", typeMapImplementation); + proj.Sources.Add (new BuildItem.Source ("InvalidUnicodeIdentifiers.cs") { + TextContent = () => """ + using Android.Runtime; + + namespace UnnamedProject; + + [Register ("com/example/1Peer")] + public class InvalidDigitStart : Java.Lang.Object + { + public InvalidDigitStart () { } + } + + [Register ("com/example/\u0301Peer")] + public class InvalidCombiningStart : Java.Lang.Object + { + public InvalidCombiningStart () { } + } + + [Register ("com/e\u0301xample/Cafe\u0301")] + public class UnsupportedCombiningMark : Java.Lang.Object + { + public UnsupportedCombiningMark () { } + } + + [Register ("com/example/A\u0cf3")] + public class Unicode15CombiningMark : Java.Lang.Object + { + public Unicode15CombiningMark () { } + } + + [Register ("com/example/\u1c89Peer")] + public class NewerUnicodeLetter : Java.Lang.Object + { + public NewerUnicodeLetter () { } + } + + [Register ("com/example/\u212bPeer")] + public class NonNfcIdentifier : Java.Lang.Object + { + public NonNfcIdentifier () { } + } + """, + }); + + using var builder = CreateApkBuilder (); + builder.ThrowOnBuildFailure = false; + Assert.IsFalse ( + builder.Build (proj), + $"{runtime}/{typeMapImplementation} should reject invalid Java identifier starts."); + StringAssertEx.Contains ($"error {expectedCode}", builder.LastBuildOutput); + + if (expectNoOutputs) { + StringAssertEx.Contains ("1Peer", builder.LastBuildOutput); + StringAssertEx.Contains ("\u0301Peer", builder.LastBuildOutput); + StringAssertEx.Contains ("e\u0301xample", builder.LastBuildOutput); + StringAssertEx.Contains ("A\u0cf3", builder.LastBuildOutput); + StringAssertEx.Contains ("\u1c89Peer", builder.LastBuildOutput); + StringAssertEx.Contains ("\u212bPeer", builder.LastBuildOutput); + AssertNoUnicodeOutputs (builder, "1Peer"); + AssertNoUnicodeOutputs (builder, "\u0301Peer"); + AssertNoUnicodeOutputs (builder, "Cafe\u0301"); + AssertNoUnicodeOutputs (builder, "A\u0cf3"); + AssertNoUnicodeOutputs (builder, "\u1c89Peer"); + AssertNoUnicodeOutputs (builder, "\u212bPeer"); + return; + } + + foreach (var javaName in new [] { + "com/example/1Peer", + "com/example/\u0301Peer", + "com/e\u0301xample/Cafe\u0301", + "com/example/A\u0cf3", + "com/example/\u1c89Peer", + "com/example/\u212bPeer", + }) { + var relativePath = (javaName + ".java").Replace ('/', Path.DirectorySeparatorChar); + var javaPath = builder.Output.GetIntermediaryPath (Path.Combine ("android", "src", relativePath)); + FileAssert.Exists (javaPath, $"llvm-ir should reach javac with '{javaName}'."); + var source = File.ReadAllText (javaPath); + StringAssert.Contains ($"public class {javaName.Substring (javaName.LastIndexOf ('/') + 1)}", source); + } + } + + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, true, false)] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, false, true)] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, false, true)] + public void Build_CanonicallyEquivalentJavaNames_DoNotOverwriteOutputs ( + string typeMapImplementation, + AndroidRuntime runtime, + bool shouldSucceed, + bool expectNoOutputs) + { + bool isRelease = runtime == AndroidRuntime.NativeAOT; + if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { + return; + } + + var proj = new XamarinAndroidApplicationProject { + IsRelease = isRelease, + }; + proj.SetRuntime (runtime); + proj.SetProperty ("AndroidTypeMapImplementation", typeMapImplementation); + proj.Sources.Add (new BuildItem.Source ("CanonicalJavaIdentifiers.cs") { + TextContent = () => """ + using Android.Runtime; + + namespace UnnamedProject; + + [Register ("com/example/\u00c5Peer")] + public class ComposedIdentifier : Java.Lang.Object + { + public ComposedIdentifier () { } + } + + [Register ("com/example/\u212bPeer")] + public class CanonicalEquivalentIdentifier : Java.Lang.Object + { + public CanonicalEquivalentIdentifier () { } + } + """, + }); + + using var builder = CreateApkBuilder (); + builder.ThrowOnBuildFailure = false; + Assert.AreEqual ( + shouldSucceed, + builder.Build (proj), + $"{runtime}/{typeMapImplementation} should match canonical-equivalent Java output behavior."); + + if (expectNoOutputs) { + StringAssertEx.Contains ("error XA4258", builder.LastBuildOutput); + StringAssertEx.Contains ("\u212bPeer", builder.LastBuildOutput); + AssertNoUnicodeOutputs (builder, "\u00c5Peer"); + AssertNoUnicodeOutputs (builder, "\u212bPeer"); + return; + } + + var javaDirectory = builder.Output.GetIntermediaryPath ( + Path.Combine ("android", "src", "com", "example")); + if (!CanonicalEquivalentPathsAlias (javaDirectory)) { + return; + } + var javaFile = AssertSingleFile (javaDirectory, "*Peer.java"); + StringAssert.Contains ("public class \u212bPeer", File.ReadAllText (javaFile)); + + var classDirectory = builder.Output.GetIntermediaryPath ( + Path.Combine ("android", "bin", "classes", "com", "example")); + AssertSingleFile (classDirectory, "*Peer.class"); + + var acwMap = File.ReadAllText (builder.Output.GetIntermediaryPath ("acw-map.txt")); + StringAssert.Contains ("UnnamedProject.ComposedIdentifier;com.example.\u00c5Peer", acwMap); + StringAssert.Contains ("UnnamedProject.CanonicalEquivalentIdentifier;com.example.\u212bPeer", acwMap); + + var dexFile = builder.Output.GetIntermediaryPath (Path.Combine ("android", "bin", "classes.dex")); + Assert.IsFalse (DexUtils.ContainsClass ("Lcom/example/\u00c5Peer;", dexFile, AndroidSdkPath)); + Assert.IsTrue (DexUtils.ContainsClass ("Lcom/example/\u212bPeer;", dexFile, AndroidSdkPath)); + + static string AssertSingleFile (string directory, string pattern) + { + var files = Directory.GetFiles (directory, pattern, SearchOption.TopDirectoryOnly); + Assert.AreEqual ( + 1, + files.Length, + "APFS should expose the canonical-equivalent llvm-ir paths as one overwritten file."); + return files [0]; + } + + static bool CanonicalEquivalentPathsAlias (string directory) + { + var probeDirectory = Path.Combine (directory, "canonical-path-probe-" + Guid.NewGuid ().ToString ("N")); + Directory.CreateDirectory (probeDirectory); + try { + File.WriteAllText (Path.Combine (probeDirectory, "\u00c5"), "composed"); + File.WriteAllText (Path.Combine (probeDirectory, "\u212b"), "equivalent"); + return Directory.GetFiles (probeDirectory).Length == 1; + } finally { + foreach (var file in Directory.GetFiles (probeDirectory)) { + File.Delete (file); + } + Directory.Delete (probeDirectory); + } + } + } + [Test] public void Build_PublishAotProject_UsesTrimmableTypeMapForCoreClrDebug () { @@ -998,6 +1354,204 @@ public ConcreteProvider (Android.Content.Context context) : base (context) { } Assert.IsTrue (builder.Build (proj), "Build should have succeeded — abstract types with protected ctors should not cause XAGTT7009."); } + [Test] + public void DexUtils_RejectsDex041Containers () + { + var directory = Path.Combine (Root, "temp", TestName); + Directory.CreateDirectory (directory); + var dexFile = Path.Combine (directory, "classes.dex"); + var header = new byte [112]; + Encoding.ASCII.GetBytes ("dex\n041\0").CopyTo (header, 0); + File.WriteAllBytes (dexFile, header); + + Assert.Throws (() => DexUtils.GetClassDescriptors (dexFile)); + } + + [TestCase ("magic-terminator")] + [TestCase ("header-size")] + [TestCase ("file-size")] + [TestCase ("map-missing")] + [TestCase ("map-section")] + [TestCase ("string-section")] + [TestCase ("data-section")] + public void DexUtils_RejectsMalformedHeaders (string malformedField) + { + var dex = CreateMinimalDex ([(byte) 'L', (byte) 'x', (byte) ';'], utf16Length: 3); + switch (malformedField) { + case "magic-terminator": + dex [7] = 1; + break; + case "header-size": + WriteUInt32 (dex, 36, 111); + break; + case "file-size": + WriteUInt32 (dex, 32, checked ((uint) dex.Length - 1)); + break; + case "map-missing": + WriteUInt32 (dex, 52, 0); + break; + case "map-section": + WriteUInt32 (dex, checked ((int) ReadUInt32 (dex, 52)), uint.MaxValue); + break; + case "string-section": + WriteUInt32 (dex, 56, uint.MaxValue); + break; + case "data-section": + WriteUInt32 (dex, 104, uint.MaxValue); + break; + default: + throw new InvalidOperationException ($"Unknown malformed DEX field '{malformedField}'."); + } + + var dexFile = WriteDexFile (dex, malformedField); + Assert.Throws (() => DexUtils.GetClassDescriptors (dexFile)); + } + + [Test] + public void DexUtils_DecodesModifiedUtf8 () + { + var bmpDex = CreateMinimalDex ( + [(byte) 'L', 0xc2, 0x80, 0xe0, 0xa0, 0x80, (byte) ';'], + utf16Length: 4); + var nullDex = CreateMinimalDex ( + [(byte) 'L', 0xc0, 0x80, (byte) ';'], + utf16Length: 3); + var supplementaryDex = CreateMinimalDex ( + [(byte) 'L', (byte) 'x', (byte) '/', 0xed, 0xa0, 0x81, 0xed, 0xb0, 0x80, (byte) ';'], + utf16Length: 6); + + Assert.AreEqual ("L\u0080\u0800;", DexUtils.GetClassDescriptors (WriteDexFile (bmpDex, "bmp")).Single ()); + Assert.AreEqual ("L\0;", DexUtils.GetClassDescriptors (WriteDexFile (nullDex, "null")).Single ()); + Assert.AreEqual ("Lx/\U00010400;", DexUtils.GetClassDescriptors (WriteDexFile (supplementaryDex, "supplementary")).Single ()); + } + + [Test] + public void DexUtils_RejectsMalformedModifiedUtf8 () + { + foreach (var (name, bytes, length) in new [] { + ("raw-null", new byte [] { (byte) 'L', 0, (byte) 'x', (byte) ';' }, 4u), + ("two-byte-overlong", new byte [] { (byte) 'L', 0xc0, 0x81, (byte) ';' }, 3u), + ("two-byte-overlong-c1", new byte [] { (byte) 'L', 0xc1, 0xbf, (byte) ';' }, 3u), + ("three-byte-overlong", new byte [] { (byte) 'L', 0xe0, 0x81, 0x81, (byte) ';' }, 3u), + ("unmatched-high", new byte [] { (byte) 'L', 0xed, 0xa0, 0x81, (byte) ';' }, 3u), + ("unmatched-low", new byte [] { (byte) 'L', 0xed, 0xb0, 0x80, (byte) ';' }, 3u), + }) { + var dexFile = WriteDexFile (CreateMinimalDex (bytes, length), name); + Assert.Throws (() => DexUtils.GetClassDescriptors (dexFile)); + } + } + + [Test] + public void DexUtils_RejectsUnboundedDeclaredStringLength () + { + var dex = CreateMinimalDex ([], uint.MaxValue); + var dexFile = WriteDexFile (dex, "huge-string"); + + Assert.Throws (() => DexUtils.GetClassDescriptors (dexFile)); + } + + string WriteDexFile (byte [] dex, string name) + { + var directory = Path.Combine (Root, "temp", TestName); + Directory.CreateDirectory (directory); + var path = Path.Combine (directory, name + ".dex"); + File.WriteAllBytes (path, dex); + return path; + } + + static byte [] CreateMinimalDex (byte [] modifiedUtf8, uint utf16Length) + { + var length = EncodeUnsignedLeb128 (utf16Length); + const int stringIdsOffset = 112; + const int typeIdsOffset = 116; + const int classDefsOffset = 120; + const int stringDataOffset = 152; + int mapOffset = stringDataOffset + length.Length + modifiedUtf8.Length + 1; + var dex = new byte [mapOffset + 16]; + Encoding.ASCII.GetBytes ("dex\n039\0").CopyTo (dex, 0); + WriteUInt32 (dex, 32, checked ((uint) dex.Length)); + WriteUInt32 (dex, 36, 112); + WriteUInt32 (dex, 40, 0x12345678); + WriteUInt32 (dex, 52, checked ((uint) mapOffset)); + WriteUInt32 (dex, 56, 1); + WriteUInt32 (dex, 60, stringIdsOffset); + WriteUInt32 (dex, 64, 1); + WriteUInt32 (dex, 68, typeIdsOffset); + WriteUInt32 (dex, 96, 1); + WriteUInt32 (dex, 100, classDefsOffset); + WriteUInt32 (dex, 104, checked ((uint) (dex.Length - stringDataOffset))); + WriteUInt32 (dex, 108, stringDataOffset); + WriteUInt32 (dex, stringIdsOffset, stringDataOffset); + length.CopyTo (dex, stringDataOffset); + modifiedUtf8.CopyTo (dex, stringDataOffset + length.Length); + WriteUInt32 (dex, mapOffset, 1); + dex [mapOffset + 4] = 0x00; + dex [mapOffset + 5] = 0x10; + WriteUInt32 (dex, mapOffset + 8, 1); + WriteUInt32 (dex, mapOffset + 12, checked ((uint) mapOffset)); + return dex; + } + + static byte [] EncodeUnsignedLeb128 (uint value) + { + var bytes = new List (); + do { + byte next = (byte) (value & 0x7f); + value >>= 7; + if (value != 0) { + next |= 0x80; + } + bytes.Add (next); + } while (value != 0); + return bytes.ToArray (); + } + + static void WriteUInt32 (byte [] data, int offset, uint value) + { + data [offset] = (byte) value; + data [offset + 1] = (byte) (value >> 8); + data [offset + 2] = (byte) (value >> 16); + data [offset + 3] = (byte) (value >> 24); + } + + static uint ReadUInt32 (byte [] data, int offset) => + (uint) ( + data [offset] | + data [offset + 1] << 8 | + data [offset + 2] << 16 | + data [offset + 3] << 24); + + static void AssertNoUnicodeOutputs (ProjectBuilder builder, string memberName) + { + var typemapDirectory = builder.Output.GetIntermediaryPath ("typemap"); + var acwMapFile = builder.Output.GetIntermediaryPath ("acw-map.txt"); + var androidSourceDirectory = builder.Output.GetIntermediaryPath (Path.Combine ("android", "src")); + var outputs = new List (); + if (Directory.Exists (typemapDirectory)) { + outputs.AddRange (Directory.GetFiles (typemapDirectory, "*.TypeMap.dll", SearchOption.AllDirectories)); + outputs.AddRange (Directory.GetFiles (typemapDirectory, "_Microsoft.Android.TypeMaps.dll", SearchOption.AllDirectories)); + } + if (File.Exists (acwMapFile)) { + outputs.Add (acwMapFile); + } + + foreach (var javaDirectory in new [] { Path.Combine (typemapDirectory, "java"), androidSourceDirectory }) { + if (!Directory.Exists (javaDirectory)) { + continue; + } + foreach (var javaFile in Directory.GetFiles (javaDirectory, "*.java", SearchOption.AllDirectories)) { + if (File.ReadAllText (javaFile).Contains (memberName, StringComparison.Ordinal)) { + outputs.Add (javaFile); + } + } + } + Assert.IsEmpty ( + outputs, + "Invalid Unicode Java names should not produce typemap assemblies, ACW maps, or partial Java output:" + + Environment.NewLine + string.Join (Environment.NewLine, outputs) + ); + } + static void AssertTrimmableTypeMapOutputs (string typemapDir) { DirectoryAssert.Exists (typemapDir); diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Utilities/DexUtils.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Utilities/DexUtils.cs index 0caea353a90..441f0d0f37c 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Utilities/DexUtils.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Utilities/DexUtils.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; @@ -62,18 +63,241 @@ Direct methods - */ /// - /// Runs the dexdump command to see if a class exists in a dex file + /// Reads the DEX type tables to see if a class exists. /// /// A Java class name of the form 'Landroid/app/ActivityTracker;' public static bool ContainsClass (string className, string dexFile, string androidSdkDirectory) { - bool containsClass = false; - DataReceivedEventHandler handler = (s, e) => { - if (e.Data != null && e.Data.Contains ("Class descriptor") && e.Data.Contains (className)) - containsClass = true; - }; - DexDump (handler, dexFile, androidSdkDirectory); - return containsClass; + _ = androidSdkDirectory; + return GetClassDescriptors (dexFile).Any (descriptor => descriptor.Contains (className, StringComparison.Ordinal)); + } + + public static IReadOnlyList GetClassDescriptors (string dexFile) + { + using var stream = File.OpenRead (dexFile); + using var reader = new BinaryReader (stream, Encoding.UTF8, leaveOpen: true); + if (stream.Length < 112 || stream.Length > uint.MaxValue) { + throw new InvalidDataException ($"'{dexFile}' is not a DEX file."); + } + var magic = reader.ReadBytes (8); + if (magic.Length != 8 || + magic [0] != 'd' || + magic [1] != 'e' || + magic [2] != 'x' || + magic [3] != '\n' || + magic [7] != 0) { + throw new InvalidDataException ($"'{dexFile}' has invalid DEX magic."); + } + var version = Encoding.ASCII.GetString (magic, 4, 3); + if (version == "041") { + throw new NotSupportedException ("DEX 041 containers with multiple logical files are not supported."); + } + if (version is not ("035" or "037" or "038" or "039" or "040")) { + throw new InvalidDataException ($"'{dexFile}' uses unsupported DEX version '{version}'."); + } + + uint fileSize = ReadUInt32 (reader, 32); + if (fileSize != stream.Length) { + throw new InvalidDataException ($"'{dexFile}' declares size {fileSize} but contains {stream.Length} bytes."); + } + uint headerSize = ReadUInt32 (reader, 36); + if (headerSize != 112) { + throw new InvalidDataException ($"'{dexFile}' declares unsupported DEX header size {headerSize}."); + } + uint endianTag = ReadUInt32 (reader, 40); + if (endianTag != 0x12345678) { + throw new InvalidDataException ($"'{dexFile}' uses unsupported DEX endianness 0x{endianTag:x8}."); + } + + ValidateSection (stream, ReadUInt32 (reader, 44), ReadUInt32 (reader, 48), 1, "link"); + uint dataSize = ReadUInt32 (reader, 104); + uint dataOffset = ReadUInt32 (reader, 108); + ValidateSection (stream, dataSize, dataOffset, 1, "data"); + uint mapOffset = ReadUInt32 (reader, 52); + if (mapOffset == 0) { + throw new InvalidDataException ("DEX file has no map list."); + } + ValidateRange (stream, mapOffset, 4, "map"); + uint mapCount = ReadUInt32 (reader, mapOffset); + if (mapCount == 0) { + throw new InvalidDataException ("DEX map list is empty."); + } + ulong mapSize = 4 + (ulong) mapCount * 12; + ValidateRange (stream, mapOffset, mapSize, "map"); + ulong dataEnd = (ulong) dataOffset + dataSize; + if (mapOffset < dataOffset || (ulong) mapOffset + mapSize > dataEnd) { + throw new InvalidDataException ("DEX map list is outside the data section."); + } + + uint stringCount = ReadUInt32 (reader, 56); + uint stringOffset = ReadUInt32 (reader, 60); + uint typeCount = ReadUInt32 (reader, 64); + uint typeOffset = ReadUInt32 (reader, 68); + ValidateSection (stream, stringCount, stringOffset, 4, "string identifiers"); + ValidateSection (stream, typeCount, typeOffset, 4, "type identifiers"); + ValidateSection (stream, ReadUInt32 (reader, 72), ReadUInt32 (reader, 76), 12, "prototype identifiers"); + ValidateSection (stream, ReadUInt32 (reader, 80), ReadUInt32 (reader, 84), 8, "field identifiers"); + ValidateSection (stream, ReadUInt32 (reader, 88), ReadUInt32 (reader, 92), 8, "method identifiers"); + uint classCount = ReadUInt32 (reader, 96); + uint classOffset = ReadUInt32 (reader, 100); + ValidateSection (stream, classCount, classOffset, 32, "class definitions"); + + var stringOffsets = ReadUInt32Table (reader, stringCount, stringOffset); + var typeDescriptorIndexes = ReadUInt32Table (reader, typeCount, typeOffset); + var descriptors = new List (checked ((int) classCount)); + for (uint index = 0; index < classCount; index++) { + uint classIndex = ReadUInt32 (reader, checked (classOffset + index * 32)); + if (classIndex >= typeDescriptorIndexes.Length) { + throw new InvalidDataException ($"DEX class index {classIndex} is outside the type table."); + } + uint descriptorIndex = typeDescriptorIndexes [classIndex]; + if (descriptorIndex >= stringOffsets.Length) { + throw new InvalidDataException ($"DEX descriptor index {descriptorIndex} is outside the string table."); + } + descriptors.Add (ReadModifiedUtf8 (reader, stringOffsets [descriptorIndex])); + } + return descriptors; + } + + static uint [] ReadUInt32Table (BinaryReader reader, uint count, uint offset) + { + if (count > int.MaxValue) { + throw new InvalidDataException ($"DEX table contains too many entries: {count}."); + } + var values = new uint [checked ((int) count)]; + reader.BaseStream.Position = offset; + for (int index = 0; index < values.Length; index++) { + values [index] = reader.ReadUInt32 (); + } + return values; + } + + static uint ReadUInt32 (BinaryReader reader, uint offset) + { + ValidateRange (reader.BaseStream, offset, 4, "value"); + reader.BaseStream.Position = offset; + return reader.ReadUInt32 (); + } + + static void ValidateSection (Stream stream, uint count, uint offset, uint itemSize, string description) + { + if (count == 0) { + return; + } + if (offset == 0) { + throw new InvalidDataException ($"DEX {description} has entries but no offset."); + } + ValidateRange (stream, offset, (ulong) count * itemSize, description); + } + + static void ValidateRange (Stream stream, uint offset, ulong size, string description) + { + ulong end = (ulong) offset + size; + if (end > (ulong) stream.Length) { + throw new InvalidDataException ($"DEX {description} at 0x{offset:x} extends beyond the file."); + } + } + + static string ReadModifiedUtf8 (BinaryReader reader, uint offset) + { + ValidateRange (reader.BaseStream, offset, 1, "string"); + reader.BaseStream.Position = offset; + uint utf16Length = ReadUnsignedLeb128 (reader); + long remainingBytes = reader.BaseStream.Length - reader.BaseStream.Position; + if (remainingBytes < 1 || utf16Length > (ulong) (remainingBytes - 1)) { + throw new InvalidDataException ( + $"DEX string declares {utf16Length} UTF-16 units with only {remainingBytes} bytes remaining."); + } + var value = new StringBuilder (); + bool pendingHighSurrogate = false; + while (true) { + byte first = ReadStringByte (reader); + if (first == 0) { + break; + } + if ((first & 0x80) == 0) { + AppendCodeUnit ((char) first); + continue; + } + if ((first & 0xe0) == 0xc0) { + byte second = ReadContinuationByte (reader); + char decoded = (char) (((first & 0x1f) << 6) | (second & 0x3f)); + if (decoded < '\u0080' && (first != 0xc0 || second != 0x80)) { + throw new InvalidDataException ("DEX modified UTF-8 contains an invalid two-byte overlong encoding."); + } + AppendCodeUnit (decoded); + continue; + } + if ((first & 0xf0) == 0xe0) { + byte second = ReadContinuationByte (reader); + byte third = ReadContinuationByte (reader); + char decoded = (char) (((first & 0x0f) << 12) | ((second & 0x3f) << 6) | (third & 0x3f)); + if (decoded < '\u0800') { + throw new InvalidDataException ("DEX modified UTF-8 contains an invalid three-byte overlong encoding."); + } + AppendCodeUnit (decoded); + continue; + } + throw new InvalidDataException ($"Invalid DEX modified UTF-8 lead byte 0x{first:x2}."); + } + if (pendingHighSurrogate) { + throw new InvalidDataException ("DEX modified UTF-8 string ends with an unmatched high surrogate."); + } + if (value.Length != utf16Length) { + throw new InvalidDataException ($"DEX string declared {utf16Length} UTF-16 units but decoded {value.Length}."); + } + return value.ToString (); + + void AppendCodeUnit (char decoded) + { + if (pendingHighSurrogate) { + if (!char.IsLowSurrogate (decoded)) { + throw new InvalidDataException ("DEX modified UTF-8 high surrogate is not followed by a low surrogate."); + } + pendingHighSurrogate = false; + } else if (char.IsLowSurrogate (decoded)) { + throw new InvalidDataException ("DEX modified UTF-8 contains an unmatched low surrogate."); + } else if (char.IsHighSurrogate (decoded)) { + pendingHighSurrogate = true; + } + if (value.Length >= utf16Length) { + throw new InvalidDataException ($"DEX string decodes to more than its declared {utf16Length} UTF-16 units."); + } + value.Append (decoded); + } + } + + static byte ReadContinuationByte (BinaryReader reader) + { + byte value = ReadStringByte (reader); + if ((value & 0xc0) != 0x80) { + throw new InvalidDataException ($"Invalid DEX modified UTF-8 continuation byte 0x{value:x2}."); + } + return value; + } + + static byte ReadStringByte (BinaryReader reader) + { + if (reader.BaseStream.Position >= reader.BaseStream.Length) { + throw new InvalidDataException ("DEX modified UTF-8 string is not null-terminated."); + } + return reader.ReadByte (); + } + + static uint ReadUnsignedLeb128 (BinaryReader reader) + { + uint value = 0; + for (int index = 0; index < 5; index++) { + byte next = ReadStringByte (reader); + if (index == 4 && (next & 0xf0) != 0) { + throw new InvalidDataException ("DEX unsigned LEB128 value exceeds 32 bits."); + } + value |= (uint) (next & 0x7f) << (index * 7); + if ((next & 0x80) == 0) { + return value; + } + } + throw new InvalidDataException ("Invalid DEX unsigned LEB128 value."); } /// diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj index 2be97ec0043..0cccc64ab71 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj @@ -113,6 +113,9 @@ Utilities\JavaNameValidator.cs + + Utilities\JavaIdentifierData.cs + Mono.Android\UsesLibraryAttribute.cs diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index 6e46aacf34a..837261f25e1 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -95,6 +95,128 @@ public void DotNetRun (bool isRelease, string typemapImplementation, AndroidRunt Assert.IsTrue (didLaunch, "Activity should have started."); } + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR)] + [TestCase ("trimmable", AndroidRuntime.CoreCLR)] + [TestCase ("trimmable", AndroidRuntime.NativeAOT)] + public void UnicodeJavaIdentifierActivityActivates (string typeMapImplementation, AndroidRuntime runtime) + { + bool isRelease = runtime == AndroidRuntime.NativeAOT; + if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { + return; + } + + const string javaName = "com.\u00e9xample.\u0394elta"; + var expectedLogcatOutput = new HashSet (StringComparer.Ordinal) { + "UNICODE_JCW_ACTIVATED=1", + "UNICODE_CURRENCY_ACTIVATED", + "UNICODE_CONNECTOR_ACTIVATED", + "UNICODE_SUPPLEMENTARY_CLASS_NOT_FOUND", + }; + var proj = new XamarinAndroidApplicationProject ( + packageName: PackageUtils.MakePackageName (runtime, "unicodeidentifier")) { + IsRelease = isRelease, + }; + proj.SetRuntime (runtime); + proj.SetRuntimeIdentifiers (new [] { DeviceAbi }); + proj.SetDefaultTargetDevice (); + proj.SetProperty ("AndroidTypeMapImplementation", typeMapImplementation); + proj.MainActivity = proj.DefaultMainActivity + .Replace ( + "[Android.Runtime.Register (\"${JAVA_PACKAGENAME}.MainActivity\"),", + $"[Android.Runtime.Register (\"{javaName}\"),") + .Replace ( + "//${FIELDS}", + """ + static int constructorInvocations; + + public MainActivity () + { + constructorInvocations++; + } + """) + .Replace ( + "//${AFTER_ONCREATE}", + """ + Android.Util.Log.Info ("UnicodeJavaIdentifiers", $"UNICODE_JCW_ACTIVATED={constructorInvocations}"); + var currencyHandle = Android.Runtime.JNIEnv.StartCreateInstance (typeof (CurrencyIdentifierPeer), "()V"); + Android.Runtime.JNIEnv.FinishCreateInstance (currencyHandle, "()V"); + using (var currency = Java.Lang.Object.GetObject ( + currencyHandle, Android.Runtime.JniHandleOwnership.TransferLocalRef)) { + } + var connectorHandle = Android.Runtime.JNIEnv.StartCreateInstance (typeof (ConnectorIdentifierPeer), "()V"); + Android.Runtime.JNIEnv.FinishCreateInstance (connectorHandle, "()V"); + using (var connector = Java.Lang.Object.GetObject ( + connectorHandle, Android.Runtime.JniHandleOwnership.TransferLocalRef)) { + } + try { + var supplementaryClass = Java.Interop.JniEnvironment.Types.FindClass ("com/example/\U00010428Peer\U00010400"); + Java.Interop.JniObjectReference.Dispose (ref supplementaryClass); + Android.Util.Log.Info ("UnicodeJavaIdentifiers", "UNICODE_SUPPLEMENTARY_UNEXPECTEDLY_LOADED"); + } catch (Java.Lang.ClassNotFoundException) { + Android.Util.Log.Info ("UnicodeJavaIdentifiers", "UNICODE_SUPPLEMENTARY_CLASS_NOT_FOUND"); + } + """); + proj.Sources.Add (new BuildItem.Source ("JavaTypeIdentifierPeers.cs") { + TextContent = () => """ + using Android.Runtime; + + namespace UnnamedProject; + + [Register ("com/example/\u00a2Peer")] + public class CurrencyIdentifierPeer : Java.Lang.Object + { + public CurrencyIdentifierPeer () + { + Android.Util.Log.Info ("UnicodeJavaIdentifiers", "UNICODE_CURRENCY_ACTIVATED"); + } + } + + [Register ("com/example/\u203fPeer")] + public class ConnectorIdentifierPeer : Java.Lang.Object + { + public ConnectorIdentifierPeer () + { + Android.Util.Log.Info ("UnicodeJavaIdentifiers", "UNICODE_CONNECTOR_ACTIVATED"); + } + } + + """, + }); + proj.AndroidJavaSources.Add (new AndroidItem.AndroidJavaSource ("com\\example\\\U00010428Peer\U00010400.java") { + Encoding = new UTF8Encoding (encoderShouldEmitUTF8Identifier: false), + TextContent = () => """ + package com.example; + + public class 𐐨Peer𐐀 {} + """, + }); + proj.OtherBuildItems.Add (new BuildItem ("ProguardConfiguration", "supplementary-name.pro") { + TextContent = () => "-keep class com.example.𐐨Peer𐐀 { *; }", + }); + + using var builder = CreateApkBuilder (); + Assert.IsTrue (builder.Install (proj), $"{runtime}/{typeMapImplementation} should install."); + var dexFile = builder.Output.GetIntermediaryPath (Path.Combine ("android", "bin", "classes.dex")); + Assert.IsTrue ( + DexUtils.ContainsClass ("Lcom/example/\U00010428Peer\U00010400;", dexFile, AndroidSdkPath), + "The exact supplementary descriptor should be present in DEX before Android fails to load it."); + + ClearAdbLogcat (); + AdbStartActivity ($"{proj.PackageName}/{javaName}"); + Assert.IsTrue ( + MonitorAdbLogcat ( + line => { + expectedLogcatOutput.RemoveWhere (expected => line.Contains (expected, StringComparison.Ordinal)); + return expectedLogcatOutput.Count == 0; + }, + Path.Combine (Root, builder.ProjectDirectory, "unicode-identifier-logcat.log"), + ActivityStartTimeoutInSeconds + ), + $"{runtime}/{typeMapImplementation} should activate every supported Unicode peer. " + + $"Missing: {string.Join (", ", expectedLogcatOutput)}" + ); + } + [Test] public void PublishReadyToRunPartial ([Values] bool isComposite) { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/AcwMapWriterTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/AcwMapWriterTests.cs index 18a14dc0609..1960e52b1d1 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/AcwMapWriterTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/AcwMapWriterTests.cs @@ -70,6 +70,23 @@ public void Write_NestedJniClass_PreservesDollarSign () Assert.Equal ("androidx.activity.result.contract.ActivityResultContracts$TakePicture;androidx.activity.result.contract.ActivityResultContracts$TakePicture", lines [2]); } + [Fact] + public void Write_UnicodeJniName_PreservesCodePoints () + { + var peer = new JavaPeerInfo { + JavaName = "com/\u00e9xample/Peer\u0394", + CompatJniName = "com/\u00e9xample/Peer\u0394", + ManagedTypeName = "Example.UnicodePeer", + ManagedTypeNamespace = "Example", + ManagedTypeShortName = "UnicodePeer", + AssemblyName = "Example", + }; + + var lines = WriteLines ([peer]); + + Assert.All (lines, line => Assert.EndsWith (";com.\u00e9xample.Peer\u0394", line)); + } + [Fact] public void Write_MultipleTypes_OrderedByManagedName () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JavaNameValidatorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JavaNameValidatorTests.cs index 2ca4c28ab0d..9508fa0cfc3 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JavaNameValidatorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JavaNameValidatorTests.cs @@ -1,4 +1,6 @@ +using System; using System.Collections.Generic; +using System.Security.Cryptography; using Xunit; namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; @@ -120,6 +122,130 @@ public void ValidNames_ReturnFalseAndEmptyIdentifier () Assert.Equal ("", nestedIdentifier); } + [Theory] + [InlineData ("com/\u00e9xample/\u0394elta")] + [InlineData ("com/example/\u00a2Peer")] + [InlineData ("com/example/\u203fPeer")] + [InlineData ("com/example/A\u0660")] + public void UnicodeIdentifiers_AreValid (string jniName) + { + Assert.False (JavaNameValidator.TryGetInvalidJniNameSegment (jniName, out var invalidIdentifier)); + Assert.Equal ("", invalidIdentifier); + } + + [Theory] + [InlineData ("com/example/\u00a2Outer$Inner")] + [InlineData ("com/example/\u203fOuter$Inner")] + public void JavaTypeOnlyStart_OnNestedType_IsValid (string jniName) + { + Assert.False (JavaNameValidator.TryGetInvalidJniSourceTypeSegment (jniName, out _)); + Assert.False (JavaNameValidator.TryGetInvalidJniTypeSegment ($"L{jniName};", out _, out _)); + Assert.False ( + JavaNameValidator.TryGetInvalidJavaSourceTypeSegment ( + JniSignatureHelper.JniNameToJavaName (jniName), + out _ + ) + ); + } + + [Theory] + [InlineData ('\u00a2', true, true)] + [InlineData ('\u203f', true, true)] + [InlineData ('\u0cf3', false, true)] + [InlineData ('\u0301', false, true)] + [InlineData ('\u0660', false, true)] + [InlineData ('\u1c89', false, false)] + public void FrozenJdk21IdentifierData_MatchesCharacterClassification ( + char value, + bool isStart, + bool isPart) + { + Assert.Equal (isStart, JavaIdentifierData.IsIdentifierStart (value)); + Assert.Equal (isPart, JavaIdentifierData.IsIdentifierPart (value)); + } + + [Theory] + [InlineData (0x10400, true, true)] + [InlineData (0x10428, true, true)] + [InlineData (0x1b132, true, true)] + public void FrozenJdk21SupplementaryIdentifierData_MatchesCharacterClassification ( + int value, + bool isStart, + bool isPart) + { + Assert.Equal (isStart, JavaIdentifierData.IsIdentifierStart (value)); + Assert.Equal (isPart, JavaIdentifierData.IsIdentifierPart (value)); + } + + [Fact] + public void FrozenJdk21IdentifierData_HasExpectedClassificationHash () + { + const int maxCodePoint = 0x10ffff; + var classification = new byte [maxCodePoint + 1]; + for (int value = 0; value <= maxCodePoint; value++) { + if (JavaIdentifierData.IsIdentifierStart (value)) { + classification [value] |= 1; + } + if (JavaIdentifierData.IsIdentifierPart (value)) { + classification [value] |= 2; + } + } + + Assert.Equal ( + "81b63b25dd80b36fcd964822c76bc18fe9c04319876d26b4ba3983f7d7090319", + Convert.ToHexString (SHA256.HashData (classification)).ToLowerInvariant () + ); + } + + [Theory] + [InlineData ("com/example/1Peer", "1Peer")] + [InlineData ("com/example/\u0301Peer", "\u0301Peer")] + [InlineData ("com/1example/Peer", "1example")] + [InlineData ("com/\u0301example/Peer", "\u0301example")] + [InlineData ("com/e\u0301xample/Cafe\u0301", "e\u0301xample")] + [InlineData ("com/example/A\u0cf3", "A\u0cf3")] + [InlineData ("com/example/\U00010428Peer\U00010400", "\U00010428Peer\U00010400")] + [InlineData ("com/example/\u1c89Peer", "\u1c89Peer")] + [InlineData ("com/example/\u212bPeer", "\u212bPeer")] + public void InvalidOrUnsupportedIdentifier_ReturnsSegment (string jniName, string expected) + { + Assert.True (JavaNameValidator.TryGetInvalidJniNameSegment (jniName, out var invalidIdentifier)); + Assert.Equal (expected, invalidIdentifier); + } + + [Theory] + [InlineData ("com.\u00a2pkg.example", "\u00a2pkg")] + [InlineData ("com.\u203fpkg.example", "\u203fpkg")] + [InlineData ("com.A\u0660.example", "A\u0660")] + [InlineData ("com.\U00010428pkg.example", "\U00010428pkg")] + public void JavaTypeOnlyIdentifiers_AreInvalidPackageSegments (string packageName, string expected) + { + Assert.True (JavaNameValidator.TryGetInvalidPackageSegment (packageName, '.', out var invalidIdentifier)); + Assert.Equal (expected, invalidIdentifier); + Assert.True ( + JavaNameValidator.TryGetInvalidJniNameSegment ( + packageName.Replace ('.', '/') + "/Peer", + out invalidIdentifier + ) + ); + Assert.Equal (expected, invalidIdentifier); + } + + [Fact] + public void ComposedAndDecomposedIdentifiers_AreNotNormalized () + { + const string composed = "com/\u00e9xample/Peer"; + const string decomposed = "com/e\u0301xample/Peer"; + const string canonicalComposed = "com/example/\u00c5Peer"; + const string canonicalEquivalent = "com/example/\u212bPeer"; + + Assert.False (JavaNameValidator.TryGetInvalidJniNameSegment (composed, out _)); + Assert.True (JavaNameValidator.TryGetInvalidJniNameSegment (decomposed, out _)); + Assert.NotEqual (composed, decomposed); + Assert.False (JavaNameValidator.TryGetInvalidJniNameSegment (canonicalComposed, out _)); + Assert.True (JavaNameValidator.TryGetInvalidJniNameSegment (canonicalEquivalent, out _)); + } + [Theory] [InlineData ("Lcom/example/Outer$for;", "com/example/Outer$for", "for")] [InlineData ("[[Lcom/example/Outer$record;", "com/example/Outer$record", "record")] @@ -132,6 +258,7 @@ public void TryGetInvalidJniTypeSegment_ReservedTypeIdentifier_ReturnsTrue (stri [Theory] [InlineData ("com.example.Outer.for", "for")] + [InlineData ("Outer$record", "record")] [InlineData ("com.example.record", "record")] [InlineData ("com.record.Example", null)] [InlineData ("int[]", null)] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs index 097cf57c252..8b4e95cc0a4 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs @@ -482,6 +482,11 @@ public class JniNameValidation [InlineData ("com/for/Example")] [InlineData ("com/example/for")] [InlineData ("com/example/record")] + [InlineData ("com/e\u0301xample/Cafe\u0301")] + [InlineData ("com/example/A\u0cf3")] + [InlineData ("com/example/\U00010428Peer\U00010400")] + [InlineData ("com/example/\u1c89Peer")] + [InlineData ("com/example/\u212bPeer")] public void ValidateJniName_InvalidName_Throws (string badJniName) { Assert.Throws (() => JniSignatureHelper.ValidateJniName (badJniName)); @@ -495,6 +500,10 @@ public void ValidateJniName_InvalidName_Throws (string badJniName) [InlineData ("com/example/$Generated")] [InlineData ("com/example/Outer$for")] [InlineData ("com/example/Outer$record")] + [InlineData ("com/\u00e9xample/\u0394elta")] + [InlineData ("com/example/\u00a2Peer")] + [InlineData ("com/example/\u203fPeer")] + [InlineData ("com/example/\u00c5Peer")] public void ValidateJniName_ValidName_DoesNotThrow (string validJniName) { JniSignatureHelper.ValidateJniName (validJniName); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs index 037683afc1f..bd8dbff5e59 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs @@ -67,6 +67,18 @@ static List GetDataAttributes (XElement? filter) .ToList (); } + [Fact] + public void ComponentName_PreservesUnicodeCodePoints () + { + const string javaName = "com/\u00e9xample/Peer\u0394"; + var peer = CreatePeer (javaName, new ComponentInfo { Kind = ComponentKind.Activity }); + + var manifest = GenerateAndLoad (CreateDefaultGenerator (), [peer]); + var activity = Assert.Single (manifest.Descendants ("activity")); + + Assert.Equal ("com.\u00e9xample.Peer\u0394", (string?) activity.Attribute (AttName)); + } + [Fact] public void Placeholders_InvalidEntryWithoutValue_WarnsXA1010 () { @@ -1177,6 +1189,41 @@ public void LibraryManifest_MergedAndRelativeNamesQualified () } } + [Fact] + public void LibraryManifest_PlaceholderEquivalentElementIsDeduplicated () + { + var gen = CreateDefaultGenerator (); + var libManifest = Path.GetTempFileName (); + try { + File.WriteAllText (libManifest, """ + + + + + + """); + gen.LibraryManifests = [libManifest]; + var template = ParseTemplate (""" + + + + + + """); + + var doc = GenerateAndLoad (gen, template: template); + var providers = doc.Root?.Element ("application")?.Elements ("provider") + .Where (element => (string?) element.Attribute (AndroidNs + "name") == "com.example.app.DuplicateProvider"); + Assert.Single (providers ?? []); + } finally { + File.Delete (libManifest); + } + } + [Fact] public void ApplicationJavaClass_Set () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 89075a5832d..aedcce33946 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -6,6 +6,7 @@ using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using System.Reflection.PortableExecutable; +using System.Xml.Linq; using Xunit; namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; @@ -52,7 +53,7 @@ public void LogUnresolvableJavaPeerSkippedWarning ( public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName) => logMessages.Add ($"XA4251: Type '{managedTypeName}' uses [JniAddNativeMethodRegistrationAttribute], which is not supported by the trimmable type map."); public void LogInvalidJavaNameError (string javaName, string invalidIdentifier) => - logMessages.Add ($"XA4258: Java name '{javaName}' contains reserved Java identifier '{invalidIdentifier}'."); + logMessages.Add ($"XA4258: Java name '{javaName}' contains invalid or unsupported Java identifier '{invalidIdentifier}'."); public void LogCustomJavaObjectError (string managedTypeName) => logMessages.Add ($"XA4212: Type `{managedTypeName}` implements `Android.Runtime.IJavaObject` but does not inherit `Java.Lang.Object` or `Java.Lang.Throwable`. This is not supported."); public void LogCustomJavaObjectWarning (string managedTypeName) => @@ -63,7 +64,10 @@ public void LogCustomJavaObjectWarning (string managedTypeName) => [InlineData ("com/for/Example", "for")] [InlineData ("com/example/for", "for")] [InlineData ("com/example/record", "record")] - public void ValidateJavaNames_ReservedIdentifier_LogsError (string javaName, string invalidIdentifier) + [InlineData ("com/1example/Peer", "1example")] + [InlineData ("com/e\u0301xample/Peer", "e\u0301xample")] + [InlineData ("com/\U00010428xample/Peer", "\U00010428xample")] + public void ValidateJavaNames_InvalidOrUnsupportedIdentifier_LogsError (string javaName, string invalidIdentifier) { var peers = new List { new JavaPeerInfo { @@ -77,7 +81,13 @@ public void ValidateJavaNames_ReservedIdentifier_LogsError (string javaName, str }; Assert.False (CreateGenerator ().ValidateJavaNames (peers)); - Assert.Contains (logMessages, message => message.Contains ($"XA4258: Java name '{javaName}' contains reserved Java identifier '{invalidIdentifier}'.")); + Assert.Contains ( + logMessages, + message => message.Contains ( + $"XA4258: Java name '{javaName}' contains invalid or unsupported Java identifier '{invalidIdentifier}'.", + StringComparison.Ordinal + ) + ); } [Fact] @@ -98,6 +108,135 @@ public void ValidateJavaNames_ContextualKeywordInPackage_IsValid () Assert.DoesNotContain (logMessages, message => message.Contains ("XA4258")); } + [Theory] + [InlineData ("com/example/\u00a2Peer")] + [InlineData ("com/example/\u203fPeer")] + public void ValidateJavaNames_JavaTypeOnlyStartWithoutComponent_IsValid (string javaName) + { + var peer = new JavaPeerInfo { + JavaName = javaName, + CompatJniName = javaName, + ManagedTypeName = "Example.Type", + ManagedTypeNamespace = "Example", + ManagedTypeShortName = "Type", + AssemblyName = "Example", + }; + + Assert.True (CreateGenerator ().ValidateJavaNames ([peer])); + Assert.DoesNotContain (logMessages, message => message.Contains ("XA4258")); + } + + [Fact] + public void ValidateJavaNames_JavaTypeOnlyNestedReferences_AreValid () + { + const string nestedName = "com/example/\u00a2Outer$Inner"; + var peer = new JavaPeerInfo { + JavaName = "com/example/Peer", + CompatJniName = "com/example/Peer", + ManagedTypeName = "Example.Type", + ManagedTypeNamespace = "Example", + ManagedTypeShortName = "Type", + AssemblyName = "Example", + JavaConstructors = [ + new JavaConstructorInfo { + JniSignature = "()V", + ConstructorIndex = 0, + ThrownNames = [nestedName], + }, + ], + JavaFields = [ + new JavaFieldInfo { + FieldName = "VALUE", + JavaTypeName = "com.example.\u00a2Outer.Inner", + JniTypeName = $"L{nestedName};", + InitializerMethodName = "GetValue", + Visibility = "public", + }, + ], + }; + + Assert.True (CreateGenerator ().ValidateJavaNames ([peer])); + Assert.DoesNotContain (logMessages, message => message.Contains ("XA4258")); + } + + [Theory] + [InlineData ("com/example/\u00a2Peer", "\u00a2Peer")] + [InlineData ("com/example/\u203fPeer", "\u203fPeer")] + public void ValidateJavaNames_JavaTypeOnlyStartOnComponent_LogsError (string javaName, string invalidIdentifier) + { + var peer = new JavaPeerInfo { + JavaName = javaName, + CompatJniName = javaName, + ManagedTypeName = "Example.Type", + ManagedTypeNamespace = "Example", + ManagedTypeShortName = "Type", + AssemblyName = "Example", + ComponentAttribute = new ComponentInfo { Kind = ComponentKind.Activity }, + }; + + Assert.False (CreateGenerator ().ValidateJavaNames ([peer])); + Assert.Contains (logMessages, message => + message.Contains ($"Java name '{javaName}' contains invalid or unsupported Java identifier '{invalidIdentifier}'.")); + } + + [Fact] + public void ValidateJavaNames_JavaTypeOnlyStartOnAbstractComponent_IsValid () + { + var peer = new JavaPeerInfo { + JavaName = "com/example/\u00a2Peer", + CompatJniName = "com/example/\u00a2Peer", + ManagedTypeName = "Example.Type", + ManagedTypeNamespace = "Example", + ManagedTypeShortName = "Type", + AssemblyName = "Example", + IsAbstract = true, + ComponentAttribute = new ComponentInfo { Kind = ComponentKind.Activity }, + }; + + Assert.True (CreateGenerator ().ValidateJavaNames ([peer])); + Assert.DoesNotContain (logMessages, message => message.Contains ("XA4258")); + } + + [Fact] + public void ValidateJavaNames_JavaTypeOnlyStartInManifestTemplate_LogsError () + { + var manifest = XDocument.Parse (""" + + + + + + """); + + Assert.False (CreateGenerator ().ValidateJavaNames ([], manifest: manifest)); + Assert.Contains (logMessages, message => + message.Contains ("Java name 'com.example.¢Peer' contains invalid or unsupported Java identifier '¢Peer'.")); + } + + [Fact] + public void ValidateJavaNames_DefaultPackageNestedThrownKeyword_LogsError () + { + var peer = new JavaPeerInfo { + JavaName = "com/example/Peer", + CompatJniName = "com/example/Peer", + ManagedTypeName = "Example.Type", + ManagedTypeNamespace = "Example", + ManagedTypeShortName = "Type", + AssemblyName = "Example", + JavaConstructors = [ + new JavaConstructorInfo { + JniSignature = "()V", + ConstructorIndex = 0, + ThrownNames = ["Outer$record"], + }, + ], + }; + + Assert.False (CreateGenerator ().ValidateJavaNames ([peer])); + Assert.Contains (logMessages, message => + message.Contains ("Java name 'Outer$record' contains invalid or unsupported Java identifier 'record'.")); + } + [Theory] [InlineData ("com/example/Outer$for", null, "for")] [InlineData ("com/example/Outer$record", null, "record")] @@ -121,7 +260,8 @@ public void ValidateJavaNames_ReservedReferencedTypeIdentifier_LogsError (string }; Assert.False (CreateGenerator ().ValidateJavaNames (peers)); - Assert.Contains (logMessages, message => message.Contains ($"XA4258: Java name '{referencedName}' contains reserved Java identifier '{invalidIdentifier}'.")); + Assert.Contains (logMessages, message => + message.Contains ($"XA4258: Java name '{referencedName}' contains invalid or unsupported Java identifier '{invalidIdentifier}'.")); } [Theory] @@ -162,7 +302,8 @@ public void ValidateJavaNames_ReservedDeferredRegistrationTypeIdentifier_LogsErr }; Assert.False (CreateGenerator ().ValidateJavaNames (peers)); - Assert.Contains (logMessages, message => message.Contains ($"XA4258: Java name '{javaName}' contains reserved Java identifier '{invalidIdentifier}'.")); + Assert.Contains (logMessages, message => + message.Contains ($"XA4258: Java name '{javaName}' contains invalid or unsupported Java identifier '{invalidIdentifier}'.")); } [Fact] @@ -204,12 +345,17 @@ public void ValidateJavaNames_ReservedEmittedTypeReferences_LogErrors () }; Assert.False (CreateGenerator ().ValidateJavaNames (peers)); - Assert.Contains (logMessages, message => message.Contains ("Java name 'com/example/Outer$for' contains reserved Java identifier 'for'.")); - Assert.Contains (logMessages, message => message.Contains ("Java name 'com/example/Outer$record' contains reserved Java identifier 'record'.")); - Assert.Contains (logMessages, message => message.Contains ("Java name 'com/example/Outer$yield' contains reserved Java identifier 'yield'.")); - Assert.Contains (logMessages, message => message.Contains ("Java name 'com/example/Outer$permits' contains reserved Java identifier 'permits'.")); - Assert.Contains (logMessages, message => message.Contains ("Java name 'com/example/Outer$sealed' contains reserved Java identifier 'sealed'.")); - Assert.Contains (logMessages, message => message.Contains ("Java name 'com.example.Outer.sealed' contains reserved Java identifier 'sealed'.")); + Assert.Contains (logMessages, message => message.Contains ("Java name 'com/example/Outer$for' contains invalid or unsupported Java identifier 'for'.")); + Assert.Contains (logMessages, message => + message.Contains ("Java name 'com/example/Outer$record' contains invalid or unsupported Java identifier 'record'.")); + Assert.Contains (logMessages, message => + message.Contains ("Java name 'com/example/Outer$yield' contains invalid or unsupported Java identifier 'yield'.")); + Assert.Contains (logMessages, message => + message.Contains ("Java name 'com/example/Outer$permits' contains invalid or unsupported Java identifier 'permits'.")); + Assert.Contains (logMessages, message => + message.Contains ("Java name 'com/example/Outer$sealed' contains invalid or unsupported Java identifier 'sealed'.")); + Assert.Contains (logMessages, message => + message.Contains ("Java name 'com.example.Outer.sealed' contains invalid or unsupported Java identifier 'sealed'.")); } [Fact] @@ -239,14 +385,15 @@ public void ValidateJavaNames_AfterDeferredRegistrationPropagation_ValidatesRegi Assert.True (basePeer.CannotRegisterInStaticConstructor); Assert.False (CreateGenerator ().ValidateJavaNames (peers)); - Assert.Contains (logMessages, message => message.Contains ("Java name 'com/example/Outer$for' contains reserved Java identifier 'for'.")); + Assert.Contains (logMessages, message => + message.Contains ("Java name 'com/example/Outer$for' contains invalid or unsupported Java identifier 'for'.")); } [Fact] public void ValidateJavaNames_ReservedApplicationJavaClassIdentifier_LogsError () { Assert.False (CreateGenerator ().ValidateJavaNames ([], "com.example.Outer.for")); - Assert.Contains (logMessages, message => message.Contains ("Java name 'com.example.Outer.for' contains reserved Java identifier 'for'.")); + Assert.Contains (logMessages, message => message.Contains ("Java name 'com.example.Outer.for' contains invalid or unsupported Java identifier 'for'.")); } [Theory] @@ -283,6 +430,29 @@ public void Execute_EmptyAssemblyList_ReturnsEmptyResults () Assert.Contains (logMessages, m => m.Contains ("No Java peer types found")); } + [Fact] + public void Execute_InvalidManifestComponentWithoutPeers_ReportsError () + { + var manifest = XDocument.Parse (""" + + + + + + """); + + var result = CreateGenerator ().Execute ( + [], + new Version (11, 0), + new HashSet (), + manifestTemplate: manifest); + + Assert.Empty (result.GeneratedAssemblies); + Assert.Empty (result.GeneratedJavaSources); + Assert.Contains (logMessages, message => message.StartsWith ("XA4258:", StringComparison.Ordinal)); + Assert.DoesNotContain (logMessages, message => message.Contains ("No Java peer types found")); + } + [Fact] public void Execute_AssemblyWithNoPeers_ReturnsEmpty () { @@ -454,7 +624,7 @@ public void Execute_ManifestPlaceholdersAreResolvedBeforeRooting () using var peReader = CreateTestFixturePEReader (); var manifestTemplate = System.Xml.Linq.XDocument.Parse (""" - + @@ -470,12 +640,120 @@ public void Execute_ManifestPlaceholdersAreResolvedBeforeRooting () PackageName: "my.app", AndroidApiLevel: "35", SupportedOSPlatformVersion: "24", - RuntimeProviderJavaName: "mono.MonoRuntimeProvider", - ManifestPlaceholders: "applicationId=my.app"), + RuntimeProviderJavaName: "mono.MonoRuntimeProvider"), manifestTemplate); var peer = result.AllPeers.First (p => p.ManagedTypeName == "MyApp.SimpleActivity"); - Assert.True (peer.IsUnconditional, "Relative manifest names should root correctly after placeholder substitution."); + Assert.True (peer.IsUnconditional, "Unresolved template packages should use PackageName before expanding relative components."); + Assert.DoesNotContain (logMessages, message => message.StartsWith ("XA4258:", StringComparison.Ordinal)); + } + + [Fact] + public void Execute_InvalidResolvedPackageBeforeRelativeComponent_ReportsBeforeOutputs () + { + using var peReader = CreateTestFixturePEReader (); + var manifestTemplate = XDocument.Parse (""" + + + + + + """); + + var result = CreateGenerator ().Execute ( + [Input ("TestFixtures", peReader)], + new Version (11, 0), + new HashSet (), + manifestConfig: new ManifestConfig ( + PackageName: "com.¢pkg", + AndroidApiLevel: "35", + SupportedOSPlatformVersion: "24", + RuntimeProviderJavaName: "mono.MonoRuntimeProvider"), + manifestTemplate: manifestTemplate); + + Assert.Empty (result.GeneratedAssemblies); + Assert.Empty (result.GeneratedJavaSources); + Assert.Contains (logMessages, message => + message.Contains ("Java name 'com.¢pkg.SimpleActivity' contains invalid or unsupported Java identifier '¢pkg'.")); + } + + [Fact] + public void Execute_ActivityAliasPlaceholdersAreResolvedBeforeValidationAndRooting () + { + using var peReader = CreateTestFixturePEReader (); + var manifestTemplate = XDocument.Parse (""" + + + + + + """); + + var result = CreateGenerator ().Execute ( + [Input ("TestFixtures", peReader)], + new Version (11, 0), + new HashSet (), + manifestConfig: new ManifestConfig ( + PackageName: "my.app", + AndroidApiLevel: "35", + SupportedOSPlatformVersion: "24", + RuntimeProviderJavaName: "mono.MonoRuntimeProvider", + ManifestPlaceholders: "targetPackage=myapp"), + manifestTemplate: manifestTemplate); + + var peer = result.AllPeers.First (candidate => candidate.ManagedTypeName == "MyApp.UnnamedActivity"); + Assert.True (peer.IsUnconditional, "The activity-alias target should root its Java peer."); + Assert.NotEmpty (result.GeneratedJavaSources); + Assert.DoesNotContain (logMessages, message => message.StartsWith ("XA4258:", StringComparison.Ordinal)); + var alias = Assert.Single (result.Manifest?.Document.Descendants ("activity-alias") ?? []); + XNamespace android = "http://schemas.android.com/apk/res/android"; + Assert.Equal ("my.app.UnnamedAlias", (string?) alias.Attribute (android + "name")); + Assert.Equal ( + JniSignatureHelper.JniNameToJavaBinaryName (peer.JavaName), + (string?) alias.Attribute (android + "targetActivity") + ); + Assert.NotEqual ("myapp.UnnamedActivity", (string?) alias.Attribute (android + "targetActivity")); + } + + [Theory] + [InlineData ("${applicationId}.¢Alias", "${applicationId}.SimpleActivity", "com.example.¢Alias", "¢Alias")] + [InlineData ("${applicationId}.SimpleAlias", "${applicationId}.¢Peer", "com.example.¢Peer", "¢Peer")] + public void Execute_InvalidActivityAliasNameOrTarget_ReportsBeforeOutputs ( + string aliasName, + string targetActivity, + string invalidName, + string invalidIdentifier) + { + using var peReader = CreateTestFixturePEReader (); + var manifestTemplate = XDocument.Parse ($$""" + + + + + + """); + + var result = CreateGenerator ().Execute ( + [Input ("TestFixtures", peReader)], + new Version (11, 0), + new HashSet (), + manifestConfig: new ManifestConfig ( + PackageName: "com.example", + AndroidApiLevel: "35", + SupportedOSPlatformVersion: "24", + RuntimeProviderJavaName: "mono.MonoRuntimeProvider"), + manifestTemplate: manifestTemplate); + + Assert.Empty (result.GeneratedAssemblies); + Assert.Empty (result.GeneratedJavaSources); + Assert.Contains (logMessages, message => + message.Contains ( + $"Java name '{invalidName}' contains invalid or unsupported Java identifier '{invalidIdentifier}'.", + StringComparison.Ordinal + ) + ); } [Theory] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index f66eb72f608..af4ffa450b7 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -54,6 +54,16 @@ public void Build_CreatesOneEntryPerPeer () Assert.Equal ("java/lang/Object", model.Entries [1].MapKey); } + [Fact] + public void Build_UnicodeJniName_PreservesCodePoints () + { + const string javaName = "com/\u00e9xample/Peer\u0394"; + var model = BuildModel ([MakePeerWithActivation (javaName, "Test.UnicodePeer", "A")]); + + Assert.Equal (javaName, Assert.Single (model.Entries).MapKey); + Assert.Equal (javaName, Assert.Single (model.ProxyTypes).JniName); + } + [Fact] public void Build_DuplicateJniNames_CreatesAliasEntries () { diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ConstructorActivationTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ConstructorActivationTests.cs index 00cbfe024c5..cec3fff3e38 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ConstructorActivationTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ConstructorActivationTests.cs @@ -88,6 +88,19 @@ public void JavaSideDefaultConstructorRunsOnceAndRegistersPeer () } } + [Test] + [Category ("UnicodeJavaIdentifiers")] + public void JavaSideUnicodeIdentifierConstructorsRunAndRegisterPeers () + { + UnicodeLetterIdentifierPeer.Reset (); + using (var instance = CreateFromJava ("()V")) { + Assert.AreEqual (1, UnicodeLetterIdentifierPeer.ConstructorInvocations); + Assert.AreEqual ("com/\u00e9xample/\u0394elta", JNIEnv.GetClassNameFromInstance (instance.Handle)); + AssertRegisteredSame (instance); + } + + } + [Test] public void ManagedConstructionDoesNotReenterThroughJavaConstructorActivation () { @@ -932,6 +945,22 @@ public static void Reset () } } + [Register ("com/\u00e9xample/\u0394elta")] + public class UnicodeLetterIdentifierPeer : Java.Lang.Object + { + public static int ConstructorInvocations; + + public UnicodeLetterIdentifierPeer () + { + ConstructorInvocations++; + } + + public static void Reset () + { + ConstructorInvocations = 0; + } + } + [Register ("net/dot/android/test/ConstructorActivationMarshalObject")] public class ConstructorActivationMarshalObject : ConstructorActivationBase {