-
Notifications
You must be signed in to change notification settings - Fork 7.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Parse numeric strings as numbers again during conversions #8681
Conversation
@lzybkr @rjmholt It seems that by calling PowerShell/src/System.Management.Automation/engine/parser/Parser.cs Lines 250 to 256 in f1218bd
This seems to be the source of the stackoverflow I'm encountering here. Not really sure how best to avoid this one, though. (ty @SeeminglyScience for double-checking my blindness and pointing this out!) Per @iSazonov's comment in #7710 I'll look to see if we can keep a Parser handy in LanguagePrimitives for this purpose, and perhaps along the way I'll find a good way to circumvent the issue here. |
That's a great idea!
The only way to stop it is going to be some way of communicating to The relevant Parser code would then be: if (shouldTryCoercion && (token == null || !tokenizer.IsAtEndOfScript(token.Extent)))
{
// We call ConvertTo, primarily because we expect it will throw an exception,
// but it's possible it could succeed, e.g. if the string had commas, our lexer
// will fail, but Convert.ChangeType could succeed.
return LanguagePrimitives.ConvertTo(str, toType, CultureInfo.InvariantCulture); |
8e2126b
to
f99a7c1
Compare
@rjmholt @lzybkr appreciate the suggestions! It now no longer overflows. However, despite also attempting to throw an exception here when handling the recursion, it seems that somehow that's getting swallowed and not emitted like other conversion failures do. Not sure what's happening there? Hmm. Also, attempting to create a static Parser object for this purpose seems to trigger an AMSI uninitialization failure?
|
♻️ Refactor parsing conversions, allow to fallback to current
f08632c
to
cf60710
Compare
Why is it so easy to break error handling? No idea. But there it is.
Okay, all fixed. I think. Let's make sure that runs correctly now... Seems OK locally. It was very surprising to me to completely break console error display by adding a default parameter to If this runs OK I'll look at adding some tests. 😄 |
🤔 OK, not sure what I broke in this test. Failure is:
I can't really figure out how/why this is failing here, unfortunately. Looking at the test, it seems like it originates from a helper module that I don't have any experience with. 😕 |
@PaulHigin Could you please help @vexx32 with the debugger security issue? Is it possible to resolve the issue in the PR context? |
@rjmholt @BrucePay @iSazonov I've looked at coding in a static parser instance to reuse for the static class methods, but it might be more trouble than it's worth. Was discussing it with @SeeminglyScience and he brought up a thread-safety concern; with a static parser instance in place there's no guarantee multiple threads won't try to use it at the same time. 😕 Thread safety isn't something I have a lot of awareness of at the moment, so I think I'll probably not try to get too deep into that here as that's more of a tangent than anything else in this PR. 😄 (Also, I did try it out and it seems to work, but it is breaking something somewhere in the builds, and I'm not sure where... 🙁 It seems that it would have to be declared in the actual Parser class, though, as declaring it in LanguagePrimitives triggers the AMSI uninit failure message.) |
358760a
to
ad6a82f
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add tests.
@@ -2907,6 +2907,17 @@ private static CultureInfo GetCultureFromFormatProvider(IFormatProvider formatPr | |||
TypeConverter integerConverter = LanguagePrimitives.GetIntegerSystemConverter(resultType); | |||
try | |||
{ | |||
try |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is used identically three times I think we should place it in a helper method. Something like "TryScanNumber()".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Definitely could... it's used in three contexts, though (int, decimal, real), and the behaviour when it hits a ParseException differs in all three. I'll see what I can come up with.
resultType, | ||
System.Globalization.CultureInfo.InvariantCulture.NumberFormat); | ||
} | ||
catch (ParseException) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Documentation says Convert.ChangeType() can throw InvalidCastException, FormatException, etc. To prevent more breakage than necessary, it seems like we should catch a generic Exception exception here and let the original code path run.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is intentional (I should probably comment it).
The original code path has an outer catch (Exception e)
block below, where the general exceptions from ChangeType() are handled. This isn't the clearest code form, I'll readily admit, but it's the simplest way to handle both errors in the right way, I think. I guess I could move the latter return
into the catch (ParseException)
block instead, but that would make it more difficult to turn into a helper method.
If there's a parse exception, the method goes on to call the general integerConverter.ConvertFrom()
method as it was before, but if there's a general exception either there or during the ChangeType()
an entirely different code path is taken... assuming I am understanding my try/catch logic there correctly. 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I mean is that the code now first tries the new TryScanNumber()
method before reverting back to the old code path (integerconverter
or ChangeType
directly on strToConvert
). It seems possible that TryScanNumber()
could throw an exception when the old code path(s) might have succeeded, causing a regression. To be safe it seems to me it is best to catch generic Exception
instead of ParseException
in TryScanNumber()
to ensure fallback code paths always get a chance at the conversion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I see what you mean, I think. So the only case I can see that happening is if ScanNumber()
succeeds in getting a value, but the subsequent Convert.ChangeType()
fails for some reason... and the subsequent code paths wouldn't have necessarily failed anyway on that error.
I would generally expect that if the ChangeType()
fails, so too would integerConverter
's method anyway, but I suppose there are potential edge cases where this wouldn't go so well... better safe than sorry, and at least in theory it shouldn't have to do the double-check too often, so it should be OK.
Will change, thank you!
I'll take care of the CodeFactor issues tomorrow. 😄 |
Co-Authored-By: vexx32 <32407840+vexx32@users.noreply.github.com>
Co-Authored-By: vexx32 <32407840+vexx32@users.noreply.github.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@vexx32 After @PaulHigin update code review please look CodeFactor issues and re-format code.
else | ||
{ | ||
return Convert.ChangeType(strToConvert, resultType, | ||
System.Globalization.CultureInfo.InvariantCulture.NumberFormat); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please format code.
@@ -2977,7 +3022,7 @@ private static CultureInfo GetCultureFromFormatProvider(IFormatProvider formatPr | |||
|
|||
throw new PSInvalidCastException("InvalidCastFromStringToDecimal", e, | |||
ExtendedTypeSystem.InvalidCastExceptionWithInnerException, | |||
valueToConvert.ToString(), resultType.ToString(), e.Message); | |||
strToConvert, resultType.ToString(), e.Message); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please format code.
} | ||
catch (Exception e) | ||
{ | ||
typeConversion.WriteLine("Exception converting to double or single: \"{0}\".", e.Message); | ||
throw new PSInvalidCastException("InvalidCastFromStringToDoubleOrSingle", e, | ||
ExtendedTypeSystem.InvalidCastExceptionWithInnerException, | ||
valueToConvert.ToString(), resultType.ToString(), e.Message); | ||
strToConvert, resultType.ToString(), e.Message); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please format code.
♻️ Tidy Convert.ChangeType() calls up a bit
@iSazonov @PaulHigin that ought to do it. The remaining CodeFactor issues are all "this looks like Hungarian notation" complaints; unless we feel the need to rename half the variables in this file, I don't think it's worth pursuing. 😄 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
@vexx32 Please create Docs issue and link in the PR description. Thanks! |
@iSazonov all done! |
PR Summary
Standardizes numeric conversions by invoking
Parser.ScanNumber()
during conversions, allowing numeric conversions to recognise and appropriately handle suffixed numerals that are specific to the parser and otherwise not known to PS's standard casting and conversion methods.shouldTryCoercion
parameter toParser.ScanNumber()
which has a default value oftrue
. When the method is called without this parameter, it will throw instead of attempting to recurse back intoLanguagePrimitives.ConvertTo()
ConvertStringToInteger()
,ConvertStringToReal()
, andConvertStringToDecimal()
inLanguagePrimitives
to attempt to parse numbers using the PS parser itself.[int]"1ulgb"
where the cast string would otherwise not be recognised as a number.[int]"1,000"
which can be converted with more conventional methods but not recognised by the parser.Resolves #7710
/cc @lzybkr @rjmholt @SteveL-MSFT
However, attempting to convert invalid values (that normally result in a parsing exception e.g.,[int]"200y"
) instead triggers aStackOverflowException
, crashing PowerShell. I don't know enough about the internals to figure out exactly what's going wrong here, would appreciate some guidance!@SeeminglyScience helped me find out where it was recursing until stack overflow; the issue is that calling
Parser.ScanNumber()
will eventually attempt to invokeLanguagePrimitives.ConvertTo()
which will eventually attempt to re-invoke the method that originally calledParser.ScanNumber()
. Resolved by adding a default parameter which instead throws an exception, and the conversion methods fall back to their original code paths.PR Context
See #7710
PR Checklist
.h
,.cpp
,.cs
,.ps1
and.psm1
files have the correct copyright headerWIP:
or[ WIP ]
to the beginning of the title (theWIP
bot will keep its status check atPending
while the prefix is present) and remove the prefix when the PR is ready.[feature]
to your commit messages if the change is significant or affects feature tests