Skip to content

Commit 4944984

Browse files
committed
fix
1 parent 98b5f53 commit 4944984

2 files changed

Lines changed: 10 additions & 24 deletions

File tree

src/FastCloner.Tests/FailureHypothesisTests.cs

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,6 @@
99
namespace FastCloner.Tests;
1010
public class FailureHypothesisTests
1111
{
12-
/// <summary>
13-
/// Demonstrates a weakness: <see cref="FastClonerSafeTypes"/> assumes any class that overrides
14-
/// <c>GetHashCode</c> has value-based hashing (<c>HasStableHashSemantics == true</c>). This drives
15-
/// hash-based collections through a memberwise (raw field) clone path that copies the internal
16-
/// <c>_slots</c>/<c>_buckets</c> arrays verbatim. When the override actually returns an identity-based
17-
/// hash (e.g. <c>RuntimeHelpers.GetHashCode(this)</c>), the cloned bucket entries store the *original*
18-
/// object's identity hash, but the elements inside are themselves deep-cloned and therefore have a
19-
/// brand-new identity hash. The cloned set/dictionary is structurally corrupt: lookups by the
20-
/// cloned key miss, even though the key is the very element stored in the clone.
21-
/// </summary>
2212
private sealed class IdentityHashedKey
2313
{
2414
public string Tag { get; set; } = "";
@@ -66,13 +56,7 @@ await Assert.That(clone.TryGetValue(cloneKey, out int value)).IsTrue()
6656
"FastCloner stores stale identity hashes from the original key in the cloned bucket.");
6757
await Assert.That(value).IsEqualTo(42);
6858
}
69-
70-
/// <summary>
71-
/// Type whose override would normally throw on a default-state probe instance (Tag is null, ToUpper NREs).
72-
/// Without an opt-in, the probe catches the throw and conservatively rebuilds the collection. With
73-
/// <see cref="FastClonerStableHashAttribute"/> the type author asserts the override is value-based, so
74-
/// FastCloner skips the probe and uses the fast memberwise path. Lookups in the cloned set must still work.
75-
/// </summary>
59+
7660
[FastClonerStableHash]
7761
private sealed class ProbeUnfriendlyButStableKey
7862
{
@@ -82,11 +66,7 @@ public override bool Equals(object? obj)
8266
=> obj is ProbeUnfriendlyButStableKey other
8367
&& string.Equals(Tag, other.Tag, StringComparison.OrdinalIgnoreCase);
8468
}
85-
86-
/// <summary>
87-
/// Same hash semantics as <see cref="ProbeUnfriendlyButStableKey"/> but without the attribute. Used to
88-
/// assert that the attribute really is what changes the verdict (not some unrelated probe success).
89-
/// </summary>
69+
9070
private sealed class ProbeUnfriendlyKeyNoAttribute
9171
{
9272
public string Tag { get; set; } = "";

src/FastCloner/Code/FastClonerGenerator.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,9 @@ private static FastClonerCache.TypeCloneMetadata BuildTypeMetadata(Type type)
270270

271271
if (cacheEntry.Cloner is not null)
272272
{
273-
return CloneRootWithTrackedState(obj, cacheEntry.Cloner, cacheEntry.Metadata!);
273+
return cacheEntry.CanUseNoTrackingState ?
274+
cacheEntry.Cloner(obj, FastCloneState.GetSimpleState()) :
275+
CloneRootWithTrackedState(obj, cacheEntry.Cloner, cacheEntry.Metadata!);
274276
}
275277
}
276278

@@ -370,7 +372,11 @@ private static bool TryCloneSafeArrayRoot<T>(T obj, Type runtimeType, out T? clo
370372
return obj;
371373
}
372374

373-
return metadata.CanSkipReferenceTracking ? cloner(obj, FastCloneState.GetSimpleState()) : CloneRootWithTrackedState(obj, cloner, metadata);
375+
if (metadata.CanSkipReferenceTracking ||
376+
(metadata is { CyclePolicy: FastClonerCache.CyclePolicy.None, HasBehaviorSensitiveMembers: false } && !rootType.IsValueType))
377+
return cloner(obj, FastCloneState.GetSimpleState());
378+
379+
return CloneRootWithTrackedState(obj, cloner, metadata);
374380
}
375381

376382
private static T CloneRootWithTrackedState<T>(T obj, Func<T, FastCloneState, T> cloner, FastClonerCache.TypeCloneMetadata metadata)

0 commit comments

Comments
 (0)