Util.py (scons-4.2.0) | : | Util.py (SCons-4.3.0) | ||
---|---|---|---|---|
skipping to change at line 120 | skipping to change at line 120 | |||
calculating command signatures. | calculating command signatures. | |||
""" | """ | |||
drive, rest = os.path.splitdrive(path) | drive, rest = os.path.splitdrive(path) | |||
if drive: | if drive: | |||
path = drive.upper() + rest | path = drive.upper() + rest | |||
return path | return path | |||
class NodeList(UserList): | class NodeList(UserList): | |||
"""A list of Nodes with special attribute retrieval. | """A list of Nodes with special attribute retrieval. | |||
This class is almost exactly like a regular list of Nodes | Unlike an ordinary list, access to a member's attribute returns a | |||
(actually it can hold any object), with one important difference. | `NodeList` containing the same attribute for each member. Although | |||
If you try to get an attribute from this list, it will return that | this can hold any object, it is intended for use when processing | |||
attribute from every item in the list. For example: | Nodes, where fetching an attribute of each member is very commone, | |||
for example getting the content signature of each node. The term | ||||
"attribute" here includes the string representation. | ||||
Example: | ||||
>>> someList = NodeList([' foo ', ' bar ']) | >>> someList = NodeList([' foo ', ' bar ']) | |||
>>> someList.strip() | >>> someList.strip() | |||
['foo', 'bar'] | ['foo', 'bar'] | |||
""" | """ | |||
def __bool__(self): | def __bool__(self): | |||
return bool(self.data) | return bool(self.data) | |||
def __str__(self): | def __str__(self): | |||
return ' '.join(map(str, self.data)) | return ' '.join(map(str, self.data)) | |||
def __iter__(self): | def __iter__(self): | |||
return iter(self.data) | return iter(self.data) | |||
def __call__(self, *args, **kwargs): | def __call__(self, *args, **kwargs) -> 'NodeList': | |||
result = [x(*args, **kwargs) for x in self.data] | result = [x(*args, **kwargs) for x in self.data] | |||
return self.__class__(result) | return self.__class__(result) | |||
def __getattr__(self, name): | def __getattr__(self, name) -> 'NodeList': | |||
"""Returns a NodeList of `name` from each member.""" | ||||
result = [getattr(x, name) for x in self.data] | result = [getattr(x, name) for x in self.data] | |||
return self.__class__(result) | return self.__class__(result) | |||
def __getitem__(self, index): | def __getitem__(self, index): | |||
""" | """Returns one item, forces a `NodeList` if `index` is a slice.""" | |||
This comes for free on py2, | # TODO: annotate return how? Union[] - don't know type of single item | |||
but py3 slices of NodeList are returning a list | ||||
breaking slicing nodelist and refering to | ||||
properties and methods on contained object | ||||
""" | ||||
# return self.__class__(self.data[index]) | ||||
if isinstance(index, slice): | if isinstance(index, slice): | |||
# Expand the slice object using range() | return self.__class__(self.data[index]) | |||
# limited by number of items in self.data | ||||
indices = index.indices(len(self.data)) | ||||
return self.__class__([self[x] for x in range(*indices)]) | ||||
# Return one item of the tart | ||||
return self.data[index] | return self.data[index] | |||
_get_env_var = re.compile(r'^\$([_a-zA-Z]\w*|{[_a-zA-Z]\w*})$') | _get_env_var = re.compile(r'^\$([_a-zA-Z]\w*|{[_a-zA-Z]\w*})$') | |||
def get_environment_var(varstr) -> Optional[str]: | def get_environment_var(varstr) -> Optional[str]: | |||
"""Return undecorated construction variable string. | """Return undecorated construction variable string. | |||
Determine if `varstr` looks like a reference | Determine if `varstr` looks like a reference | |||
to a single environment variable, like `"$FOO"` or `"${FOO}"`. | to a single environment variable, like `"$FOO"` or `"${FOO}"`. | |||
If so, return that variable with no decorations, like `"FOO"`. | If so, return that variable with no decorations, like `"FOO"`. | |||
skipping to change at line 1192 | skipping to change at line 1186 | |||
22 ['-', '-', 's', 'o', 'm', 'e', ' ', '-', '-', 'o', 'p', 't', 's', ' ', 'a ', 'n', 'd', ' ', 'a', 'r', 'g', 's'] | 22 ['-', '-', 's', 'o', 'm', 'e', ' ', '-', '-', 'o', 'p', 't', 's', ' ', 'a ', 'n', 'd', ' ', 'a', 'r', 'g', 's'] | |||
>>> c = CLVar("--some --opts and args") | >>> c = CLVar("--some --opts and args") | |||
>>> print(len(c), repr(c)) | >>> print(len(c), repr(c)) | |||
4 ['--some', '--opts', 'and', 'args'] | 4 ['--some', '--opts', 'and', 'args'] | |||
>>> c += " strips spaces " | >>> c += " strips spaces " | |||
>>> print(len(c), repr(c)) | >>> print(len(c), repr(c)) | |||
6 ['--some', '--opts', 'and', 'args', 'strips', 'spaces'] | 6 ['--some', '--opts', 'and', 'args', 'strips', 'spaces'] | |||
""" | """ | |||
def __init__(self, initlist=None): | def __init__(self, initlist=None): | |||
super().__init__(Split(initlist)) | super().__init__(Split(initlist if initlist is not None else [])) | |||
def __add__(self, other): | def __add__(self, other): | |||
return super().__add__(CLVar(other)) | return super().__add__(CLVar(other)) | |||
def __radd__(self, other): | def __radd__(self, other): | |||
return super().__radd__(CLVar(other)) | return super().__radd__(CLVar(other)) | |||
def __iadd__(self, other): | def __iadd__(self, other): | |||
return super().__iadd__(CLVar(other)) | return super().__iadd__(CLVar(other)) | |||
skipping to change at line 1642 | skipping to change at line 1636 | |||
obj.added_methods.append(method) | obj.added_methods.append(method) | |||
else: | else: | |||
method = MethodType(function, obj) | method = MethodType(function, obj) | |||
else: | else: | |||
# obj is a class | # obj is a class | |||
method = function | method = function | |||
setattr(obj, name, method) | setattr(obj, name, method) | |||
# Default hash function and format. SCons-internal. | # Default hash function and format. SCons-internal. | |||
ALLOWED_HASH_FORMATS = ['md5', 'sha1', 'sha256'] | DEFAULT_HASH_FORMATS = ['md5', 'sha1', 'sha256'] | |||
ALLOWED_HASH_FORMATS = [] | ||||
_HASH_FUNCTION = None | _HASH_FUNCTION = None | |||
_HASH_FORMAT = None | _HASH_FORMAT = None | |||
def _attempt_init_of_python_3_9_hash_object(hash_function_object, sys_used=sys): | ||||
"""Python 3.9 and onwards lets us initialize the hash function object with t | ||||
he | ||||
key "usedforsecurity"=false. This lets us continue to use algorithms that ha | ||||
ve | ||||
been deprecated either by FIPS or by Python itself, as the MD5 algorithm SCo | ||||
ns | ||||
prefers is not being used for security purposes as much as a short, 32 char | ||||
hash that is resistant to accidental collisions. | ||||
In prior versions of python, hashlib returns a native function wrapper, whic | ||||
h | ||||
errors out when it's queried for the optional parameter, so this function | ||||
wraps that call. | ||||
It can still throw a ValueError if the initialization fails due to FIPS | ||||
compliance issues, but that is assumed to be the responsibility of the calle | ||||
r. | ||||
""" | ||||
if hash_function_object is None: | ||||
return None | ||||
# https://stackoverflow.com/a/11887885 details how to check versions with th | ||||
e "packaging" library. | ||||
# however, for our purposes checking the version is greater than or equal to | ||||
3.9 is good enough, as | ||||
# the API is guaranteed to have support for the 'usedforsecurity' flag in 3. | ||||
9. See | ||||
# https://docs.python.org/3/library/hashlib.html#:~:text=usedforsecurity for | ||||
the version support notes. | ||||
if (sys_used.version_info.major > 3) or (sys_used.version_info.major == 3 an | ||||
d sys_used.version_info.minor >= 9): | ||||
return hash_function_object(usedforsecurity=False) | ||||
# note that this can throw a ValueError in FIPS-enabled versions of Linux pr | ||||
ior to 3.9 | ||||
# the OpenSSL hashlib will throw on first init here, but that is assumed to | ||||
be responsibility of | ||||
# the caller to diagnose the ValueError & potentially display the error to s | ||||
creen. | ||||
return hash_function_object() | ||||
def _set_allowed_viable_default_hashes(hashlib_used, sys_used=sys): | ||||
"""Checks if SCons has ability to call the default algorithms normally suppo | ||||
rted. | ||||
This util class is sometimes called prior to setting the user-selected hash | ||||
algorithm, | ||||
meaning that on FIPS-compliant systems the library would default-initialize | ||||
MD5 | ||||
and throw an exception in set_hash_format. A common case is using the SConf | ||||
options, | ||||
which can run prior to main, and thus ignore the options.hash_format variabl | ||||
e. | ||||
This function checks the DEFAULT_HASH_FORMATS and sets the ALLOWED_HASH_FORM | ||||
ATS | ||||
to only the ones that can be called. In Python >= 3.9 this will always defau | ||||
lt to | ||||
MD5 as in Python 3.9 there is an optional attribute "usedforsecurity" set fo | ||||
r the method. | ||||
Throws if no allowed hash formats are detected. | ||||
""" | ||||
global ALLOWED_HASH_FORMATS | ||||
_last_error = None | ||||
# note: if you call this method repeatedly, example using timeout, this is n | ||||
eeded. | ||||
# otherwise it keeps appending valid formats to the string | ||||
ALLOWED_HASH_FORMATS = [] | ||||
for test_algorithm in DEFAULT_HASH_FORMATS: | ||||
_test_hash = getattr(hashlib_used, test_algorithm, None) | ||||
# we know hashlib claims to support it... check to see if we can call it | ||||
. | ||||
if _test_hash is not None: | ||||
# the hashing library will throw an exception on initialization in F | ||||
IPS mode, | ||||
# meaning if we call the default algorithm returned with no paramete | ||||
rs, it'll | ||||
# throw if it's a bad algorithm, otherwise it will append it to the | ||||
known | ||||
# good formats. | ||||
try: | ||||
_attempt_init_of_python_3_9_hash_object(_test_hash, sys_used) | ||||
ALLOWED_HASH_FORMATS.append(test_algorithm) | ||||
except ValueError as e: | ||||
_last_error = e | ||||
continue | ||||
if len(ALLOWED_HASH_FORMATS) == 0: | ||||
from SCons.Errors import SConsEnvironmentError # pylint: disable=import | ||||
-outside-toplevel | ||||
# chain the exception thrown with the most recent error from hashlib. | ||||
raise SConsEnvironmentError( | ||||
'No usable hash algorithms found.' | ||||
'Most recent error from hashlib attached in trace.' | ||||
) from _last_error | ||||
return | ||||
_set_allowed_viable_default_hashes(hashlib) | ||||
def get_hash_format(): | def get_hash_format(): | |||
"""Retrieves the hash format or ``None`` if not overridden. | """Retrieves the hash format or ``None`` if not overridden. | |||
A return value of ``None`` | A return value of ``None`` | |||
does not guarantee that MD5 is being used; instead, it means that the | does not guarantee that MD5 is being used; instead, it means that the | |||
default precedence order documented in :func:`SCons.Util.set_hash_format` | default precedence order documented in :func:`SCons.Util.set_hash_format` | |||
is respected. | is respected. | |||
""" | """ | |||
return _HASH_FORMAT | return _HASH_FORMAT | |||
def set_hash_format(hash_format): | def _attempt_get_hash_function(hash_name, hashlib_used=hashlib, sys_used=sys): | |||
"""Wrapper used to try to initialize a hash function given. | ||||
If successful, returns the name of the hash function back to the user. | ||||
Otherwise returns None. | ||||
""" | ||||
try: | ||||
_fetch_hash = getattr(hashlib_used, hash_name, None) | ||||
if _fetch_hash is None: | ||||
return None | ||||
_attempt_init_of_python_3_9_hash_object(_fetch_hash, sys_used) | ||||
return hash_name | ||||
except ValueError: | ||||
# if attempt_init_of_python_3_9 throws, this is typically due to FIPS be | ||||
ing enabled | ||||
# however, if we get to this point, the viable hash function check has e | ||||
ither been | ||||
# bypassed or otherwise failed to properly restrict the user to only the | ||||
supported | ||||
# functions. As such throw the UserError as an internal assertion-like e | ||||
rror. | ||||
return None | ||||
def set_hash_format(hash_format, hashlib_used=hashlib, sys_used=sys): | ||||
"""Sets the default hash format used by SCons. | """Sets the default hash format used by SCons. | |||
If `hash_format` is ``None`` or | If `hash_format` is ``None`` or | |||
an empty string, the default is determined by this function. | an empty string, the default is determined by this function. | |||
Currently the default behavior is to use the first available format of | Currently the default behavior is to use the first available format of | |||
the following options: MD5, SHA1, SHA256. | the following options: MD5, SHA1, SHA256. | |||
""" | """ | |||
global _HASH_FORMAT, _HASH_FUNCTION | global _HASH_FORMAT, _HASH_FUNCTION | |||
_HASH_FORMAT = hash_format | _HASH_FORMAT = hash_format | |||
if hash_format: | if hash_format: | |||
hash_format_lower = hash_format.lower() | hash_format_lower = hash_format.lower() | |||
if hash_format_lower not in ALLOWED_HASH_FORMATS: | if hash_format_lower not in ALLOWED_HASH_FORMATS: | |||
from SCons.Errors import UserError # pylint: disable=import-outside -toplevel | from SCons.Errors import UserError # pylint: disable=import-outside -toplevel | |||
raise UserError('Hash format "%s" is not supported by SCons. Only ' | # user can select something not supported by their OS but normally s | |||
upported by | ||||
# SCons, example, selecting MD5 in an OS with FIPS-mode turned on. T | ||||
herefore we first | ||||
# check if SCons supports it, and then if their local OS supports it | ||||
. | ||||
if hash_format_lower in DEFAULT_HASH_FORMATS: | ||||
raise UserError('While hash format "%s" is supported by SCons, t | ||||
he ' | ||||
'local system indicates only the following hash ' | ||||
'formats are supported by the hashlib library: %s' % | ||||
(hash_format_lower, | ||||
', '.join(ALLOWED_HASH_FORMATS)) | ||||
) | ||||
else: | ||||
# the hash format isn't supported by SCons in any case. Warn the | ||||
user, and | ||||
# if we detect that SCons supports more algorithms than their lo | ||||
cal system | ||||
# supports, warn the user about that too. | ||||
if ALLOWED_HASH_FORMATS == DEFAULT_HASH_FORMATS: | ||||
raise UserError('Hash format "%s" is not supported by SCons. | ||||
Only ' | ||||
'the following hash formats are supported: %s' % | 'the following hash formats are supported: %s' % | |||
(hash_format_lower, | (hash_format_lower, | |||
', '.join(ALLOWED_HASH_FORMATS))) | ', '.join(ALLOWED_HASH_FORMATS)) | |||
) | ||||
else: | ||||
raise UserError('Hash format "%s" is not supported by SCons. | ||||
' | ||||
'SCons supports more hash formats than your local sy | ||||
stem ' | ||||
'is reporting; SCons supports: %s. Your local system | ||||
only ' | ||||
'supports: %s' % | ||||
(hash_format_lower, | ||||
', '.join(DEFAULT_HASH_FORMATS), | ||||
', '.join(ALLOWED_HASH_FORMATS)) | ||||
) | ||||
# this is not expected to fail. If this fails it means the set_allowed_v | ||||
iable_default_hashes | ||||
# function did not throw, or when it threw, the exception was caught and | ||||
ignored, or | ||||
# the global ALLOWED_HASH_FORMATS was changed by an external user. | ||||
_HASH_FUNCTION = _attempt_get_hash_function(hash_format_lower, hashlib_u | ||||
sed, sys_used) | ||||
_HASH_FUNCTION = getattr(hashlib, hash_format_lower, None) | ||||
if _HASH_FUNCTION is None: | if _HASH_FUNCTION is None: | |||
from SCons.Errors import UserError # pylint: disable=import-outside -toplevel | from SCons.Errors import UserError # pylint: disable=import-outside -toplevel | |||
raise UserError( | raise UserError( | |||
'Hash format "%s" is not available in your Python interpreter.' | 'Hash format "%s" is not available in your Python interpreter. ' | |||
'Expected to be supported algorithm by set_allowed_viable_defaul | ||||
t_hashes, ' | ||||
'Assertion error in SCons.' | ||||
% hash_format_lower | % hash_format_lower | |||
) | ) | |||
else: | else: | |||
# Set the default hash format based on what is available, defaulting | # Set the default hash format based on what is available, defaulting | |||
# to md5 for backwards compatibility. | # to the first supported hash algorithm (usually md5) for backwards comp | |||
atibility. | ||||
# in FIPS-compliant systems this usually defaults to SHA1, unless that t | ||||
oo has been | ||||
# disabled. | ||||
for choice in ALLOWED_HASH_FORMATS: | for choice in ALLOWED_HASH_FORMATS: | |||
_HASH_FUNCTION = getattr(hashlib, choice, None) | _HASH_FUNCTION = _attempt_get_hash_function(choice, hashlib_used, sy | |||
s_used) | ||||
if _HASH_FUNCTION is not None: | if _HASH_FUNCTION is not None: | |||
break | break | |||
else: | else: | |||
# This is not expected to happen in practice. | # This is not expected to happen in practice. | |||
from SCons.Errors import UserError # pylint: disable=import-outside -toplevel | from SCons.Errors import UserError # pylint: disable=import-outside -toplevel | |||
raise UserError( | raise UserError( | |||
'Your Python interpreter does not have MD5, SHA1, or SHA256. ' | 'Your Python interpreter does not have MD5, SHA1, or SHA256. ' | |||
'SCons requires at least one.') | 'SCons requires at least one. Expected to support one or more ' | |||
'during set_allowed_viable_default_hashes.' | ||||
) | ||||
# Ensure that this is initialized in case either: | # Ensure that this is initialized in case either: | |||
# 1. This code is running in a unit test. | # 1. This code is running in a unit test. | |||
# 2. This code is running in a consumer that does hash operations while | # 2. This code is running in a consumer that does hash operations while | |||
# SConscript files are being loaded. | # SConscript files are being loaded. | |||
set_hash_format(None) | set_hash_format(None) | |||
def _get_hash_object(hash_format): | def get_current_hash_algorithm_used(): | |||
"""Returns the current hash algorithm name used. | ||||
Where the python version >= 3.9, this is expected to return md5. | ||||
If python's version is <= 3.8, this returns md5 on non-FIPS-mode platforms, | ||||
and | ||||
sha1 or sha256 on FIPS-mode Linux platforms. | ||||
This function is primarily useful for testing, where one expects a value to | ||||
be | ||||
one of N distinct hashes, and therefore the test needs to know which hash to | ||||
select. | ||||
""" | ||||
return _HASH_FUNCTION | ||||
def _get_hash_object(hash_format, hashlib_used=hashlib, sys_used=sys): | ||||
"""Allocates a hash object using the requested hash format. | """Allocates a hash object using the requested hash format. | |||
Args: | Args: | |||
hash_format: Hash format to use. | hash_format: Hash format to use. | |||
Returns: | Returns: | |||
hashlib object. | hashlib object. | |||
""" | """ | |||
if hash_format is None: | if hash_format is None: | |||
if _HASH_FUNCTION is None: | if _HASH_FUNCTION is None: | |||
from SCons.Errors import UserError # pylint: disable=import-outside -toplevel | from SCons.Errors import UserError # pylint: disable=import-outside -toplevel | |||
raise UserError('There is no default hash function. Did you call ' | raise UserError('There is no default hash function. Did you call ' | |||
'a hashing function before SCons was initialized?') | 'a hashing function before SCons was initialized?') | |||
return _HASH_FUNCTION() | return _attempt_init_of_python_3_9_hash_object(getattr(hashlib_used, _HA SH_FUNCTION, None), sys_used) | |||
if not hasattr(hashlib, hash_format): | if not hasattr(hashlib, hash_format): | |||
from SCons.Errors import UserError # pylint: disable=import-outside-top level | from SCons.Errors import UserError # pylint: disable=import-outside-top level | |||
raise UserError( | raise UserError( | |||
'Hash format "%s" is not available in your Python interpreter.' % | 'Hash format "%s" is not available in your Python interpreter.' % | |||
hash_format) | hash_format) | |||
return getattr(hashlib, hash_format)() | return _attempt_init_of_python_3_9_hash_object(getattr(hashlib, hash_format) , sys_used) | |||
def hash_signature(s, hash_format=None): | def hash_signature(s, hash_format=None): | |||
""" | """ | |||
Generate hash signature of a string | Generate hash signature of a string | |||
Args: | Args: | |||
s: either string or bytes. Normally should be bytes | s: either string or bytes. Normally should be bytes | |||
hash_format: Specify to override default hash format | hash_format: Specify to override default hash format | |||
Returns: | Returns: | |||
End of changes. 19 change blocks. | ||||
33 lines changed or deleted | 222 lines changed or added |