mirror of
https://github.com/AdaCore/cpython.git
synced 2026-02-12 12:57:15 -08:00
The regex to decode a number in fpformat is susceptible to catastrophic backtracking. This is a potential DOS vector if a server is using fpformat on untrusted number strings. Replace it with an equivalent non-vulnerable regex. The match behavior of the new regex is slightly different. It captures the whole integer part of the number in one group, Leading zeros are stripped off later.
147 lines
4.6 KiB
Python
147 lines
4.6 KiB
Python
"""General floating point formatting functions.
|
|
|
|
Functions:
|
|
fix(x, digits_behind)
|
|
sci(x, digits_behind)
|
|
|
|
Each takes a number or a string and a number of digits as arguments.
|
|
|
|
Parameters:
|
|
x: number to be formatted; or a string resembling a number
|
|
digits_behind: number of digits behind the decimal point
|
|
"""
|
|
from warnings import warnpy3k
|
|
warnpy3k("the fpformat module has been removed in Python 3.0", stacklevel=2)
|
|
del warnpy3k
|
|
|
|
import re
|
|
|
|
__all__ = ["fix","sci","NotANumber"]
|
|
|
|
# Compiled regular expression to "decode" a number
|
|
decoder = re.compile(r'^([-+]?)(\d*)((?:\.\d*)?)(([eE][-+]?\d+)?)$')
|
|
# \0 the whole thing
|
|
# \1 leading sign or empty
|
|
# \2 digits left of decimal point
|
|
# \3 fraction (empty or begins with point)
|
|
# \4 exponent part (empty or begins with 'e' or 'E')
|
|
|
|
try:
|
|
class NotANumber(ValueError):
|
|
pass
|
|
except TypeError:
|
|
NotANumber = 'fpformat.NotANumber'
|
|
|
|
def extract(s):
|
|
"""Return (sign, intpart, fraction, expo) or raise an exception:
|
|
sign is '+' or '-'
|
|
intpart is 0 or more digits beginning with a nonzero
|
|
fraction is 0 or more digits
|
|
expo is an integer"""
|
|
res = decoder.match(s)
|
|
if res is None: raise NotANumber, s
|
|
sign, intpart, fraction, exppart = res.group(1,2,3,4)
|
|
intpart = intpart.lstrip('0');
|
|
if sign == '+': sign = ''
|
|
if fraction: fraction = fraction[1:]
|
|
if exppart: expo = int(exppart[1:])
|
|
else: expo = 0
|
|
return sign, intpart, fraction, expo
|
|
|
|
def unexpo(intpart, fraction, expo):
|
|
"""Remove the exponent by changing intpart and fraction."""
|
|
if expo > 0: # Move the point left
|
|
f = len(fraction)
|
|
intpart, fraction = intpart + fraction[:expo], fraction[expo:]
|
|
if expo > f:
|
|
intpart = intpart + '0'*(expo-f)
|
|
elif expo < 0: # Move the point right
|
|
i = len(intpart)
|
|
intpart, fraction = intpart[:expo], intpart[expo:] + fraction
|
|
if expo < -i:
|
|
fraction = '0'*(-expo-i) + fraction
|
|
return intpart, fraction
|
|
|
|
def roundfrac(intpart, fraction, digs):
|
|
"""Round or extend the fraction to size digs."""
|
|
f = len(fraction)
|
|
if f <= digs:
|
|
return intpart, fraction + '0'*(digs-f)
|
|
i = len(intpart)
|
|
if i+digs < 0:
|
|
return '0'*-digs, ''
|
|
total = intpart + fraction
|
|
nextdigit = total[i+digs]
|
|
if nextdigit >= '5': # Hard case: increment last digit, may have carry!
|
|
n = i + digs - 1
|
|
while n >= 0:
|
|
if total[n] != '9': break
|
|
n = n-1
|
|
else:
|
|
total = '0' + total
|
|
i = i+1
|
|
n = 0
|
|
total = total[:n] + chr(ord(total[n]) + 1) + '0'*(len(total)-n-1)
|
|
intpart, fraction = total[:i], total[i:]
|
|
if digs >= 0:
|
|
return intpart, fraction[:digs]
|
|
else:
|
|
return intpart[:digs] + '0'*-digs, ''
|
|
|
|
def fix(x, digs):
|
|
"""Format x as [-]ddd.ddd with 'digs' digits after the point
|
|
and at least one digit before.
|
|
If digs <= 0, the point is suppressed."""
|
|
if type(x) != type(''): x = repr(x)
|
|
try:
|
|
sign, intpart, fraction, expo = extract(x)
|
|
except NotANumber:
|
|
return x
|
|
intpart, fraction = unexpo(intpart, fraction, expo)
|
|
intpart, fraction = roundfrac(intpart, fraction, digs)
|
|
while intpart and intpart[0] == '0': intpart = intpart[1:]
|
|
if intpart == '': intpart = '0'
|
|
if digs > 0: return sign + intpart + '.' + fraction
|
|
else: return sign + intpart
|
|
|
|
def sci(x, digs):
|
|
"""Format x as [-]d.dddE[+-]ddd with 'digs' digits after the point
|
|
and exactly one digit before.
|
|
If digs is <= 0, one digit is kept and the point is suppressed."""
|
|
if type(x) != type(''): x = repr(x)
|
|
sign, intpart, fraction, expo = extract(x)
|
|
if not intpart:
|
|
while fraction and fraction[0] == '0':
|
|
fraction = fraction[1:]
|
|
expo = expo - 1
|
|
if fraction:
|
|
intpart, fraction = fraction[0], fraction[1:]
|
|
expo = expo - 1
|
|
else:
|
|
intpart = '0'
|
|
else:
|
|
expo = expo + len(intpart) - 1
|
|
intpart, fraction = intpart[0], intpart[1:] + fraction
|
|
digs = max(0, digs)
|
|
intpart, fraction = roundfrac(intpart, fraction, digs)
|
|
if len(intpart) > 1:
|
|
intpart, fraction, expo = \
|
|
intpart[0], intpart[1:] + fraction[:-1], \
|
|
expo + len(intpart) - 1
|
|
s = sign + intpart
|
|
if digs > 0: s = s + '.' + fraction
|
|
e = repr(abs(expo))
|
|
e = '0'*(3-len(e)) + e
|
|
if expo < 0: e = '-' + e
|
|
else: e = '+' + e
|
|
return s + 'e' + e
|
|
|
|
def test():
|
|
"""Interactive test run."""
|
|
try:
|
|
while 1:
|
|
x, digs = input('Enter (x, digs): ')
|
|
print x, fix(x, digs), sci(x, digs)
|
|
except (EOFError, KeyboardInterrupt):
|
|
pass
|