Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Documentation/docs-mobile/messages/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
18 changes: 15 additions & 3 deletions Documentation/docs-mobile/messages/xa4258.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Original file line number Diff line number Diff line change
Expand Up @@ -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<byte> (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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.IO;
using System.Reflection;

using Xamarin.Android.Tools.Bytecode;
Expand All @@ -15,6 +16,52 @@ public void Constructor_Exceptions ()
{
Assert.Throws<ArgumentNullException> (() => 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<InvalidDataException> (() => 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;
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ static string GetRelativePath (JavaPeerInfo type)

/// <summary>
/// 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).
/// </summary>
static void WritePackageDeclaration (JavaPeerInfo type, TextWriter writer)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System;
using System.Collections.Generic;
using System.Xml.Linq;

namespace Microsoft.Android.Sdk.TrimmableTypeMap;
Expand All @@ -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<string> ComponentElementNames = new (StringComparer.Ordinal) {
"application",
"activity",
"activity-alias",
"instrumentation",
"service",
"receiver",
"provider",
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> ComponentElementNames = new (StringComparer.Ordinal) {
"application",
"activity",
"instrumentation",
"service",
"receiver",
"provider",
};

/// <summary>Warning code for library-manifest merge failures (maps to XA4302).</summary>
internal const int LibraryManifestMergeWarningCode = 4302;
Expand Down Expand Up @@ -60,6 +52,7 @@ class ManifestGenerator
// own package when they are relative (start with '.'). Mirrors ManifestDocument.ManifestAttributeFixups.
static readonly Dictionary<string, string []> ManifestAttributeFixups = new (StringComparer.Ordinal) {
{ "activity", ["name"] },
{ "activity-alias", ["name", "targetActivity"] },
{ "application", ["backupAgent"] },
{ "instrumentation", ["name"] },
{ "provider", ["name"] },
Expand All @@ -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);

Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -320,20 +317,22 @@ void RewriteCompatNames (XElement manifest, IReadOnlyList<JavaPeerInfo> 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)
Expand All @@ -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",
Expand Down Expand Up @@ -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");
Expand Down
Loading