Skip to content

Commit df5c3ee

Browse files
Rewrite trimmable typemap JNI metadata
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 8d1ab29 commit df5c3ee

5 files changed

Lines changed: 645 additions & 16 deletions

File tree

src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/JniRemapping/JniAssemblyRewriterTests.cs

Lines changed: 278 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,24 @@ static string FirstAttributeStringArg (MetadataReader reader, CustomAttributeHan
6464
return args.Count > 0 ? args [0] : null;
6565
}
6666

67+
static IReadOnlyList<string> AttributeStringArrayArg (MetadataReader reader, CustomAttributeHandleCollection attributes, EntityHandle ctor)
68+
{
69+
foreach (CustomAttributeHandle handle in attributes) {
70+
CustomAttribute attribute = reader.GetCustomAttribute (handle);
71+
if (attribute.Constructor != ctor) {
72+
continue;
73+
}
74+
75+
var decoded = attribute.DecodeValue (Xamarin.Android.Tasks.DummyCustomAttributeProvider.Instance);
76+
var result = new List<string> ();
77+
foreach (var element in (ImmutableArray<CustomAttributeTypedArgument<object>>) decoded.FixedArguments [0].Value) {
78+
result.Add ((string) element.Value);
79+
}
80+
return result;
81+
}
82+
return [];
83+
}
84+
6785
static List<KeyValuePair<int, string>> LoadedStrings (PEReader peReader, MetadataReader reader, MethodDefinitionHandle method)
6886
{
6987
var result = new List<KeyValuePair<int, string>> ();
@@ -86,10 +104,13 @@ static List<KeyValuePair<int, string>> LoadedStrings (PEReader peReader, Metadat
86104
return result;
87105
}
88106

89-
static void AssertTableRowCountsMatch (MetadataReader expected, MetadataReader actual)
107+
static void AssertTableRowCountsMatch (MetadataReader expected, MetadataReader actual, params TableIndex [] except)
90108
{
91109
for (int i = 0; i < MetadataTokens.TableCount; i++) {
92110
var table = (TableIndex) i;
111+
if (Array.IndexOf (except, table) >= 0) {
112+
continue;
113+
}
93114
Assert.AreEqual (expected.GetTableRowCount (table), actual.GetTableRowCount (table), $"Row count of table '{table}' changed.");
94115
}
95116
}
@@ -389,6 +410,194 @@ public void RewritesAttributesAndLoadedStrings ()
389410
CollectionAssert.AreEqual (new [] { "b:()V:n_Run" }, ValuesOf (LoadedStrings (peReader, reader, run)));
390411
}
391412

413+
[Test]
414+
public void RewritesTrimmableTypeMapKeysAndAliases ()
415+
{
416+
var fixture = new JniFixtureBuilder ();
417+
fixture.Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, fixture.TypeMapCtor3,
418+
fixture.AttributeBlob ("acme/orig/MyView[1]", "Acme.Proxy, Fixture", "Acme.Target, Fixture"));
419+
420+
int fieldStart = fixture.NextFieldRid;
421+
int methodStart = fixture.NextMethodRid;
422+
TypeDefinitionHandle aliasHolder = fixture.AddType ("Acme", "AliasHolder", fieldStart, methodStart);
423+
fixture.Metadata.AddCustomAttribute (aliasHolder, fixture.JavaPeerAliasesCtor1,
424+
fixture.StringArrayAttributeBlob ("acme/orig/MyView[0]", "acme/orig/MyView[1]", "unmapped/Type[0]"));
425+
426+
const string mappingText = "acme.orig.MyView -> a.b.C:\n";
427+
R8Mapping mapping = Mapping (mappingText);
428+
JniRewriteResult result = Rewrite (fixture.Serialize (), mapping);
429+
AssertReverseScanMatchesRewrite (result.Image, mapping, mappingText);
430+
using var peReader = new PEReader (ImmutableArray.Create (result.Image));
431+
MetadataReader reader = peReader.GetMetadataReader ();
432+
433+
CollectionAssert.AreEqual (new [] { "a/b/C[1]", "Acme.Proxy, Fixture", "Acme.Target, Fixture" },
434+
AttributeStringArgs (reader, reader.GetAssemblyDefinition ().GetCustomAttributes (), fixture.TypeMapCtor3));
435+
CollectionAssert.AreEqual (new [] { "a/b/C[0]", "a/b/C[1]", "unmapped/Type[0]" },
436+
AttributeStringArrayArg (reader, reader.GetTypeDefinition (aliasHolder).GetCustomAttributes (), fixture.JavaPeerAliasesCtor1));
437+
}
438+
439+
[Test]
440+
public void IdentifiesUtf8FieldRvaDataStructurally ()
441+
{
442+
var fixture = new JniFixtureBuilder ();
443+
FieldDefinitionHandle nameField = fixture.AddUtf8Field ("onClick");
444+
FieldDefinitionHandle signatureField = fixture.AddUtf8Field ("(Lacme/orig/Callback;)V");
445+
FieldDefinitionHandle embeddedNullField = fixture.AddUtf8Field ("onClick\0not-padding");
446+
447+
using var peReader = new PEReader (ImmutableArray.Create (fixture.Serialize ()));
448+
MetadataReader reader = peReader.GetMetadataReader ();
449+
FieldRvaTable table = FieldRvaTable.Read (peReader, reader);
450+
451+
Assert.AreEqual (3, table.Entries.Count);
452+
453+
FieldRvaEntry name = table.Get (nameField);
454+
Assert.IsNotNull (name);
455+
Assert.IsTrue (name.IsUtf8Datum, "A __utf8_N mapped field must be recognised structurally.");
456+
Assert.AreEqual ("onClick", name.Utf8Value);
457+
458+
FieldRvaEntry signature = table.Get (signatureField);
459+
Assert.IsNotNull (signature);
460+
Assert.IsTrue (signature.IsUtf8Datum);
461+
Assert.AreEqual ("(Lacme/orig/Callback;)V", signature.Utf8Value);
462+
463+
FieldRvaEntry embeddedNull = table.Get (embeddedNullField);
464+
Assert.IsNotNull (embeddedNull);
465+
Assert.IsFalse (embeddedNull.IsUtf8Datum, "Non-zero data after the first NUL is not rewrite padding.");
466+
}
467+
468+
[Test]
469+
public void RewritesUtf8FieldRvaJniNamesAndSignatures ()
470+
{
471+
var fixture = new JniFixtureBuilder ();
472+
473+
FieldDefinitionHandle nameField = fixture.AddUtf8Field ("onClick");
474+
FieldDefinitionHandle signatureField = fixture.AddUtf8Field ("(Lacme/orig/Callback;)V");
475+
FieldDefinitionHandle classNameField = fixture.AddUtf8Field ("acme/orig/Callback");
476+
FieldDefinitionHandle longNameField = fixture.AddUtf8Field ("run");
477+
478+
int fieldStart = fixture.NextFieldRid;
479+
int methodStart = fixture.NextMethodRid;
480+
int ctorBody = fixture.EmitBody (encoder => {
481+
encoder.OpCode (ILOpCode.Ldarg_0);
482+
encoder.LoadString (fixture.String ("acme/orig/MyView"));
483+
encoder.OpCode (ILOpCode.Pop);
484+
encoder.OpCode (ILOpCode.Ret);
485+
});
486+
fixture.AddVoidMethod (".ctor", ctorBody,
487+
MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName);
488+
489+
int registerBody = fixture.EmitBody (encoder => {
490+
encoder.OpCode (ILOpCode.Ldsflda);
491+
encoder.Token (nameField);
492+
encoder.OpCode (ILOpCode.Ldsflda);
493+
encoder.Token (signatureField);
494+
encoder.OpCode (ILOpCode.Pop);
495+
encoder.OpCode (ILOpCode.Pop);
496+
encoder.OpCode (ILOpCode.Ldsflda);
497+
encoder.Token (longNameField);
498+
encoder.OpCode (ILOpCode.Ldsflda);
499+
encoder.Token (signatureField);
500+
encoder.OpCode (ILOpCode.Pop);
501+
encoder.OpCode (ILOpCode.Pop);
502+
encoder.OpCode (ILOpCode.Ret);
503+
});
504+
fixture.AddVoidMethod ("RegisterNatives", registerBody);
505+
506+
// A JavaPeerProxy-derived type carries its JNI identity in its .ctor's only ldstr.
507+
fixture.AddType ("Acme.Orig", "MyViewProxy", fieldStart, methodStart,
508+
TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Class, fixture.JavaPeerProxyReference);
509+
510+
byte [] source = fixture.Serialize ();
511+
const string mappingText =
512+
"acme.orig.MyView -> a.b.C:\n" +
513+
" void onClick(acme.orig.Callback) -> a\n" +
514+
" void run(acme.orig.Callback) -> aMuchLongerObfuscatedName\n" +
515+
"acme.orig.Callback -> a.b.Cb:\n";
516+
R8Mapping mapping = Mapping (mappingText);
517+
JniRewriteResult result = Rewrite (source, mapping);
518+
using (var rewrittenReader = new PEReader (ImmutableArray.Create (result.Image))) {
519+
MetadataReader rewrittenMetadata = rewrittenReader.GetMetadataReader ();
520+
FieldRvaTable rewrittenFields = FieldRvaTable.Read (rewrittenReader, rewrittenMetadata);
521+
Assert.IsTrue (rewrittenFields.Get (nameField)?.IsUtf8Datum, "Rewritten method-name FieldRVA data should remain structurally recognizable.");
522+
Assert.IsTrue (rewrittenFields.Get (signatureField)?.IsUtf8Datum, "Rewritten signature FieldRVA data should remain structurally recognizable.");
523+
}
524+
AssertReverseScanMatchesRewrite (result.Image, mapping, mappingText);
525+
526+
using var peReader = new PEReader (ImmutableArray.Create (result.Image));
527+
MetadataReader reader = peReader.GetMetadataReader ();
528+
529+
Assert.AreEqual ("a", ReadUtf8Field (peReader, reader, nameField), "The method name is renamed using the owning proxy's JNI class.");
530+
Assert.AreEqual ("(La/b/Cb;)V", ReadUtf8Field (peReader, reader, signatureField));
531+
Assert.AreEqual ("a/b/Cb", ReadUtf8Field (peReader, reader, classNameField), "An unreferenced datum that is a known class name is still renamed.");
532+
Assert.AreEqual ("aMuchLongerObfuscatedName", ReadUtf8Field (peReader, reader, longNameField), "A longer datum is relocated into a wider __utf8_N slot.");
533+
534+
// Growing a datum appends exactly one new sized type; no existing token moves.
535+
using var sourceReader = new PEReader (ImmutableArray.Create (source));
536+
MetadataReader before = sourceReader.GetMetadataReader ();
537+
Assert.AreEqual (before.GetTableRowCount (TableIndex.TypeDef) + 1, reader.GetTableRowCount (TableIndex.TypeDef));
538+
AssertTableRowCountsMatch (before, reader, TableIndex.TypeDef, TableIndex.ClassLayout, TableIndex.NestedClass);
539+
}
540+
541+
static string ReadUtf8Field (PEReader peReader, MetadataReader reader, FieldDefinitionHandle field)
542+
{
543+
FieldDefinition definition = reader.GetFieldDefinition (field);
544+
int rva = definition.GetRelativeVirtualAddress ();
545+
Assert.AreNotEqual (0, rva, "Field has no RVA.");
546+
547+
PEMemoryBlock block = peReader.GetSectionData (rva);
548+
var bytes = new List<byte> ();
549+
BlobReader blob = block.GetReader (0, Math.Min (block.Length, 256));
550+
for (byte b = blob.ReadByte (); b != 0; b = blob.ReadByte ()) {
551+
bytes.Add (b);
552+
}
553+
return System.Text.Encoding.UTF8.GetString (bytes.ToArray ());
554+
}
555+
556+
[Test]
557+
public void FailsWhenASharedUtf8DatumNeedsTwoDifferentNames ()
558+
{
559+
var fixture = new JniFixtureBuilder ();
560+
561+
FieldDefinitionHandle shared = fixture.AddUtf8Field ("go");
562+
FieldDefinitionHandle signature = fixture.AddUtf8Field ("()V");
563+
564+
AddProxy (fixture, "acme/orig/P1", shared, signature);
565+
AddProxy (fixture, "acme/orig/P2", shared, signature);
566+
567+
var exception = Assert.Throws<JniRewriteException> (() => Rewrite (fixture.Serialize (), Mapping (
568+
"acme.orig.P1 -> a.b.P1:\n" +
569+
" void go() -> z\n" +
570+
"acme.orig.P2 -> a.b.P2:\n" +
571+
" void go() -> q\n")));
572+
StringAssert.Contains ("shared", exception.Message.ToLowerInvariant ());
573+
}
574+
575+
static void AddProxy (JniFixtureBuilder fixture, string jniName, FieldDefinitionHandle nameField, FieldDefinitionHandle signatureField)
576+
{
577+
int fieldStart = fixture.NextFieldRid;
578+
int methodStart = fixture.NextMethodRid;
579+
580+
fixture.AddVoidMethod (".ctor", fixture.EmitBody (encoder => {
581+
encoder.OpCode (ILOpCode.Ldarg_0);
582+
encoder.LoadString (fixture.String (jniName));
583+
encoder.OpCode (ILOpCode.Pop);
584+
encoder.OpCode (ILOpCode.Ret);
585+
}), MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName);
586+
587+
fixture.AddVoidMethod ("RegisterNatives", fixture.EmitBody (encoder => {
588+
encoder.OpCode (ILOpCode.Ldsflda);
589+
encoder.Token (nameField);
590+
encoder.OpCode (ILOpCode.Ldsflda);
591+
encoder.Token (signatureField);
592+
encoder.OpCode (ILOpCode.Pop);
593+
encoder.OpCode (ILOpCode.Pop);
594+
encoder.OpCode (ILOpCode.Ret);
595+
}));
596+
597+
fixture.AddType ("Acme.Orig", jniName.Replace ('/', '_'), fieldStart, methodStart,
598+
TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Class, fixture.JavaPeerProxyReference);
599+
}
600+
392601
[Test]
393602
public void SharedLoadedStringGetsOwnerSpecificReplacements ()
394603
{
@@ -769,6 +978,74 @@ public void RewrittenAssemblyStillMatchesItsPortablePdb ()
769978
}
770979
}
771980

981+
[Test]
982+
public void PreservesMappedFieldDataThatIsNotAJniDatum ()
983+
{
984+
var fixture = new JniFixtureBuilder ();
985+
986+
// A plain C#-style array initializer blob: not a __utf8_N datum, so it must survive
987+
// byte-for-byte.
988+
var payload = new byte [] { 0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04 };
989+
TypeDefinitionHandle enclosing = fixture.EnsurePrivateImplementationDetails ();
990+
int fieldStart = fixture.NextFieldRid;
991+
int methodStart = fixture.NextMethodRid;
992+
TypeDefinitionHandle arrayType = fixture.AddType (null, "__StaticArrayInitTypeSize=8", fieldStart, methodStart,
993+
TypeAttributes.NestedPrivate | TypeAttributes.ExplicitLayout | TypeAttributes.Sealed | TypeAttributes.AnsiClass,
994+
fixture.ValueTypeReference);
995+
fixture.Metadata.AddTypeLayout (arrayType, packingSize: 1, size: (uint) payload.Length);
996+
fixture.Metadata.AddNestedType (arrayType, enclosing);
997+
998+
var signature = new BlobBuilder ();
999+
new BlobEncoder (signature).FieldSignature ().Type (arrayType, isValueType: true);
1000+
int rva = fixture.MappedFieldData.Count;
1001+
fixture.MappedFieldData.WriteBytes (payload);
1002+
FieldDefinitionHandle dataField = fixture.Metadata.AddFieldDefinition (
1003+
FieldAttributes.Static | FieldAttributes.Assembly | FieldAttributes.HasFieldRVA,
1004+
fixture.Metadata.GetOrAddString ("ArrayData"), fixture.Metadata.GetOrAddBlob (signature));
1005+
fixture.Metadata.AddFieldRelativeVirtualAddress (dataField, rva);
1006+
1007+
JniRewriteResult result = Rewrite (fixture.Serialize (), Mapping ("acme.orig.Nothing -> a.b.N:\n"));
1008+
1009+
using var peReader = new PEReader (ImmutableArray.Create (result.Image));
1010+
MetadataReader reader = peReader.GetMetadataReader ();
1011+
int newRva = reader.GetFieldDefinition (dataField).GetRelativeVirtualAddress ();
1012+
Assert.AreNotEqual (0, newRva);
1013+
CollectionAssert.AreEqual (payload, peReader.GetSectionData (newRva).GetReader (0, payload.Length).ReadBytes (payload.Length));
1014+
}
1015+
1016+
[Test]
1017+
public void RejectsFieldRvaValueTypeWithoutAnExplicitClassLayoutSize ()
1018+
{
1019+
// A mapped value type with no ClassLayout row (or a zero size) cannot be sized safely:
1020+
// summing its instance fields would be a guess about the CLR's actual layout, and a
1021+
// wrong guess risks truncating - or reading past the end of - the mapped data. The
1022+
// rewriter must refuse rather than take that risk.
1023+
var fixture = new JniFixtureBuilder ();
1024+
1025+
TypeDefinitionHandle enclosing = fixture.EnsurePrivateImplementationDetails ();
1026+
int fieldStart = fixture.NextFieldRid;
1027+
int methodStart = fixture.NextMethodRid;
1028+
TypeDefinitionHandle unsizedType = fixture.AddType (null, "__UnsizedBlob", fieldStart, methodStart,
1029+
TypeAttributes.NestedPrivate | TypeAttributes.ExplicitLayout | TypeAttributes.Sealed | TypeAttributes.AnsiClass,
1030+
fixture.ValueTypeReference);
1031+
fixture.Metadata.AddNestedType (unsizedType, enclosing);
1032+
// Deliberately no fixture.Metadata.AddTypeLayout (...) call: the type has no
1033+
// ClassLayout row at all.
1034+
1035+
var signature = new BlobBuilder ();
1036+
new BlobEncoder (signature).FieldSignature ().Type (unsizedType, isValueType: true);
1037+
int rva = fixture.MappedFieldData.Count;
1038+
fixture.MappedFieldData.WriteBytes (new byte [] { 0x01, 0x02, 0x03, 0x04 });
1039+
FieldDefinitionHandle dataField = fixture.Metadata.AddFieldDefinition (
1040+
FieldAttributes.Static | FieldAttributes.Assembly | FieldAttributes.HasFieldRVA,
1041+
fixture.Metadata.GetOrAddString ("UnsizedData"), fixture.Metadata.GetOrAddBlob (signature));
1042+
fixture.Metadata.AddFieldRelativeVirtualAddress (dataField, rva);
1043+
1044+
byte [] source = fixture.Serialize ();
1045+
var ex = Assert.Throws<JniRewriteException> (() => Rewrite (source, Mapping ("acme.orig.Nothing -> a.b.N:\n")));
1046+
StringAssert.Contains ("ClassLayout", ex.Message);
1047+
}
1048+
7721049
[Test]
7731050
public void RewrittenAssemblyLoadsAndRunsInTheRuntime ()
7741051
{

0 commit comments

Comments
 (0)