Skip to content

Commit c79885b

Browse files
[typemap] Validate manifest aliases and DEX names
Resolve manifest placeholders before validation and alias rewriting, decode DEX and classfile modified UTF-8 exactly, and record the supplementary class-loader limitation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 5856965 commit c79885b

16 files changed

Lines changed: 716 additions & 95 deletions

File tree

Documentation/docs-mobile/messages/xa4258.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ with the .NET runtime used to build the app.
2222

2323
.NET for Android requires Normalization Form C (NFC) names and rejects combining and format
2424
characters because normalizing file systems can change or merge generated Java source paths.
25-
Supplementary characters are also rejected because Android tooling does not retain those class
26-
names in DEX files.
25+
Supplementary characters are rejected because Android's class loader cannot resolve classes with
26+
supplementary characters in their simple name, even when the exact descriptor is present in DEX.
2727

2828
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]`.
2929

external/Java.Interop/src/Xamarin.Android.Tools.Bytecode/ConstantPool.cs

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -384,31 +384,36 @@ public ConstantPoolUtf8Item (ConstantPool constantPool, Stream stream)
384384
for (int i = 0; i < data.Length; ++i)
385385
data [i] = stream.ReadNetworkByte ();
386386

387-
// The .class file specially encodes NUL so that it takes 2 bytes, not 1.
388-
// http://en.wikipedia.org/wiki/UTF-8#Modified_UTF-8
389-
var fixup = new List<byte> (data.Length);
387+
// Modified UTF-8 encodes UTF-16 code units, so supplementary characters remain
388+
// a high-surrogate/low-surrogate pair after decoding.
389+
var decoded = new StringBuilder (data.Length);
390390
for (int i = 0; i < data.Length; ++i) {
391-
if (data [i] == 0xc0 && (i + 1) < data.Length && data [i + 1] == 0x80) {
392-
fixup.Add (0x00);
393-
i++;
391+
byte first = data [i];
392+
if ((first & 0x80) == 0) {
393+
decoded.Append ((char) first);
394394
continue;
395395
}
396-
// ...and they couldn't be bothered with supporting 4-byte UTF-8 sequences,
397-
// needed for Emoji and chars off the Basic Multilingual Plane; instead, they're
398-
// encoded as a surrogate pair. (What is this I don't even...)
399-
if (data [i] == 0xed && i+6 < data.Length && data [i+3] == 0xed) {
400-
var surrogatePair = new char [] {
401-
(char) (0xD800 + (((data [i+1] & 0x0F) << 6) | (data [i+2] & 0x3F))),
402-
(char) (0xDC00 + (((data [i+4] & 0x0F) << 6) | (data [i+5] & 0x3F))),
403-
};
404-
fixup.AddRange (Encoding.UTF8.GetBytes (surrogatePair));
405-
i += 5;
396+
if ((first & 0xe0) == 0xc0 && i + 1 < data.Length) {
397+
byte second = ReadContinuationByte (data [++i]);
398+
decoded.Append ((char) (((first & 0x1f) << 6) | (second & 0x3f)));
399+
continue;
400+
}
401+
if ((first & 0xf0) == 0xe0 && i + 2 < data.Length) {
402+
byte second = ReadContinuationByte (data [++i]);
403+
byte third = ReadContinuationByte (data [++i]);
404+
decoded.Append ((char) (((first & 0x0f) << 12) | ((second & 0x3f) << 6) | (third & 0x3f)));
406405
continue;
407-
408406
}
409-
fixup.Add (data [i]);
407+
throw new InvalidDataException ($"Invalid modified UTF-8 lead byte 0x{first:x2}.");
410408
}
411-
value = Encoding.UTF8.GetString (fixup.Count == data.Length ? data : fixup.ToArray ());
409+
value = decoded.ToString ();
410+
}
411+
412+
static byte ReadContinuationByte (byte value)
413+
{
414+
if ((value & 0xc0) != 0x80)
415+
throw new InvalidDataException ($"Invalid modified UTF-8 continuation byte 0x{value:x2}.");
416+
return value;
412417
}
413418

414419
public override ConstantPoolItemType Type {

external/Java.Interop/tests/Xamarin.Android.Tools.Bytecode-Tests/ClassFileTests.cs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.IO;
23
using System.Reflection;
34

45
using Xamarin.Android.Tools.Bytecode;
@@ -15,6 +16,31 @@ public void Constructor_Exceptions ()
1516
{
1617
Assert.Throws<ArgumentNullException> (() => new ClassFile (null));
1718
}
19+
20+
[Test]
21+
public void ModifiedUtf8_DecodesUtf16CodeUnits ()
22+
{
23+
using var poolStream = new MemoryStream (new byte [] { 0, 1 });
24+
var pool = new ConstantPool (poolStream);
25+
26+
Assert.AreEqual (
27+
"\ud000\ud001",
28+
Decode (pool, new byte [] { 0xed, 0x80, 0x80, 0xed, 0x80, 0x81 })
29+
);
30+
Assert.AreEqual (
31+
"\U00010400",
32+
Decode (pool, new byte [] { 0xed, 0xa0, 0x81, 0xed, 0xb0, 0x80 })
33+
);
34+
35+
static string Decode (ConstantPool pool, byte [] bytes)
36+
{
37+
using var stream = new MemoryStream ();
38+
stream.WriteByte ((byte) (bytes.Length >> 8));
39+
stream.WriteByte ((byte) bytes.Length);
40+
stream.Write (bytes, 0, bytes.Length);
41+
stream.Position = 0;
42+
return new ConstantPoolUtf8Item (pool, stream).Value;
43+
}
44+
}
1845
}
1946
}
20-

src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestConstants.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System;
2+
using System.Collections.Generic;
13
using System.Xml.Linq;
24

35
namespace Microsoft.Android.Sdk.TrimmableTypeMap;
@@ -6,4 +8,13 @@ static class ManifestConstants
68
{
79
public static readonly XNamespace AndroidNs = "http://schemas.android.com/apk/res/android";
810
public static readonly XName AttName = AndroidNs + "name";
11+
public static readonly HashSet<string> ComponentElementNames = new (StringComparer.Ordinal) {
12+
"application",
13+
"activity",
14+
"activity-alias",
15+
"instrumentation",
16+
"service",
17+
"receiver",
18+
"provider",
19+
};
920
}

src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,6 @@ class ManifestGenerator
1616
static readonly XNamespace AndroidNs = ManifestConstants.AndroidNs;
1717
static readonly XName AttName = ManifestConstants.AttName;
1818
static readonly char [] PlaceholderSeparators = [';'];
19-
static readonly HashSet<string> ComponentElementNames = new (StringComparer.Ordinal) {
20-
"application",
21-
"activity",
22-
"instrumentation",
23-
"service",
24-
"receiver",
25-
"provider",
26-
};
2719

2820
/// <summary>Warning code for library-manifest merge failures (maps to XA4302).</summary>
2921
internal const int LibraryManifestMergeWarningCode = 4302;
@@ -60,6 +52,7 @@ class ManifestGenerator
6052
// own package when they are relative (start with '.'). Mirrors ManifestDocument.ManifestAttributeFixups.
6153
static readonly Dictionary<string, string []> ManifestAttributeFixups = new (StringComparer.Ordinal) {
6254
{ "activity", ["name"] },
55+
{ "activity-alias", ["name", "targetActivity"] },
6356
{ "application", ["backupAgent"] },
6457
{ "instrumentation", ["name"] },
6558
{ "provider", ["name"] },
@@ -83,6 +76,9 @@ class ManifestGenerator
8376
}
8477

8578
EnsureManifestAttributes (manifest);
79+
// Template component names must be resolved before compat-name rewriting. Apply again
80+
// after library-manifest merging so placeholders introduced by libraries are also covered.
81+
ApplyPlaceholders (doc, ManifestPlaceholders, PackageName);
8682
var app = EnsureApplicationElement (manifest);
8783
var targetSdkVersionValue = GetTargetSdkVersionValue (manifest);
8884

@@ -320,20 +316,22 @@ void RewriteCompatNames (XElement manifest, IReadOnlyList<JavaPeerInfo> allPeers
320316
continue;
321317
}
322318

323-
var nameAttr = element.Attribute (AttName);
324-
if (nameAttr is null) {
319+
var classNameAttr = element.Name.LocalName == "activity-alias"
320+
? element.Attribute (AndroidNs + "targetActivity")
321+
: element.Attribute (AttName);
322+
if (classNameAttr is null) {
325323
continue;
326324
}
327-
var resolved = ManifestNameResolver.Resolve (nameAttr.Value, packageName);
325+
var resolved = ManifestNameResolver.Resolve (classNameAttr.Value, packageName);
328326
if (compatToCrc.TryGetValue (resolved, out var crcName)) {
329-
nameAttr.Value = crcName;
327+
classNameAttr.Value = crcName;
330328
}
331329
}
332330
}
333331

334332
static bool IsComponentElement (XElement element)
335333
{
336-
return element.Name.NamespaceName.Length == 0 && ComponentElementNames.Contains (element.Name.LocalName);
334+
return element.Name.NamespaceName.Length == 0 && ManifestConstants.ComponentElementNames.Contains (element.Name.LocalName);
337335
}
338336

339337
void EnsureManifestAttributes (XElement manifest)

0 commit comments

Comments
 (0)