Conversation
…e conversions Every explicit IConvertible.ToXxx(IFormatProvider) implementation on AvroDecimal called Convert.ToXxx(this, provider). Because 'this' is an IConvertible, Convert.ToXxx(IConvertible, provider) calls back into the same IConvertible.ToXxx(provider) method, recursing until a StackOverflowException (which is uncatchable and crashes the process). Reproducer: Convert.ToByte(new AvroDecimal(0)). Route the 12 numeric/boolean conversions through IConvertible.ToType(type, provider) (the one method that performs the real conversion via Convert.ChangeType on the underlying decimal), and make ToString(provider) use the public ToString() override. Adds a test exercising all conversions.
zcsizmadia
left a comment
There was a problem hiding this comment.
Thanks @iemejia, nice catch. The fix is correct. Routing through IConvertible.ToType matches what the public static ToXxx(AvroDecimal) helpers and the explicit operators already do via ToType<T>(), so the explicit interface methods now agree with the rest of the type. I ran the new test against main's AvroDecimal.cs and it takes the test host down with a stack overflow, so it does guard the regression.
One thing I'd like to see addressed, here or in a follow-up: IConvertible.ToString(IFormatProvider) now ignores provider and formats with CultureInfo.CurrentCulture. With a scale > 0 value this is visible:
CultureInfo.CurrentCulture = new CultureInfo("de-DE");
Convert.ToString(new AvroDecimal(1.5m), CultureInfo.InvariantCulture); // "1,5", expected "1.5"The new test doesn't catch it because 42 has no decimal separator. IFormattable.ToString(string, IFormatProvider) has the same issue already, so it may be cleaner to fix both together. For example, add a ToString(IFormatProvider) that uses NumberFormatInfo.GetInstance(provider) for both the D{n} formatting and the separator, and have ToString() pass CultureInfo.CurrentCulture.
Minor: every assertion in the test uses a scale-0 value. Adding one fractional case (e.g. new AvroDecimal(1.5m) → ToDouble == 1.5, ToString(InvariantCulture) == "1.5") would cover the division/remainder path in ToType and the separator handling.
Happy to approve as-is if you'd rather track the provider handling separately.
What
Every explicit
IConvertible.ToXxx(IFormatProvider)implementation onAvroDecimalcalledConvert.ToXxx(this, provider). Sincethisis anIConvertible,Convert.ToXxx(IConvertible, provider)calls straight back into the sameIConvertible.ToXxx(provider)method → unbounded recursion →StackOverflowException, which is uncatchable in .NET and crashes the process.Reproducer (from the issue):
This affected
ToBoolean, ToByte, ToDecimal, ToDouble, ToInt16/32/64, ToSByte, ToSingle, ToUInt16/32/64andToString.Fix
IConvertible.ToType(Type, provider)is the only implementation that does real work (converts the unscaled/scaled value to adecimaland callsConvert.ChangeType). Route the 12 numeric/boolean conversions through it (preserving theIFormatProvider), and makeToString(provider)use the publicToString()override. NoConvert.To*(this, provider)self-calls remain.Tests
Adds
TestAvroDecimalIConvertibleDoesNotRecurseexercising all conversions (each would previously overflow the stack). Full C# suite passes (1528 tests, 0 failures).JIRA: https://issues.apache.org/jira/browse/AVRO-3569