mirror of https://github.com/crytic/slither
commit
2658a6b58d
@ -0,0 +1,12 @@ |
||||
|
||||
class ChildExpression: |
||||
def __init__(self): |
||||
super(ChildExpression, self).__init__() |
||||
self._expression = None |
||||
|
||||
def set_expression(self, expression): |
||||
self._expression = expression |
||||
|
||||
@property |
||||
def expression(self): |
||||
return self._expression |
@ -0,0 +1,36 @@ |
||||
import re |
||||
from slither.formatters.exceptions import FormatError |
||||
from slither.formatters.utils.patches import create_patch |
||||
|
||||
def format(slither, result): |
||||
elements = result['elements'] |
||||
for element in elements: |
||||
if element['type'] != "function": |
||||
# Skip variable elements |
||||
continue |
||||
target_contract = slither.get_contract_from_name(element['type_specific_fields']['parent']['name']) |
||||
if target_contract: |
||||
function = target_contract.get_function_from_signature(element['type_specific_fields']['signature']) |
||||
if function: |
||||
_patch(slither, |
||||
result, |
||||
element['source_mapping']['filename_absolute'], |
||||
int(function.parameters_src.source_mapping['start'] + |
||||
function.parameters_src.source_mapping['length']), |
||||
int(function.returns_src.source_mapping['start'])) |
||||
|
||||
|
||||
def _patch(slither, result, in_file, modify_loc_start, modify_loc_end): |
||||
in_file_str = slither.source_code[in_file].encode('utf8') |
||||
old_str_of_interest = in_file_str[modify_loc_start:modify_loc_end] |
||||
# Find the keywords view|pure|constant and remove them |
||||
m = re.search("(view|pure|constant)", old_str_of_interest.decode('utf-8')) |
||||
if m: |
||||
create_patch(result, |
||||
in_file, |
||||
modify_loc_start + m.span()[0], |
||||
modify_loc_start + m.span()[1], |
||||
m.groups(0)[0], # this is view|pure|constant |
||||
"") |
||||
else: |
||||
raise FormatError("No view/pure/constant specifier exists. Regex failed to remove specifier!") |
@ -0,0 +1,69 @@ |
||||
import re |
||||
from slither.formatters.exceptions import FormatImpossible |
||||
from slither.formatters.utils.patches import create_patch |
||||
|
||||
# Indicates the recommended versions for replacement |
||||
REPLACEMENT_VERSIONS = ["^0.4.25", "^0.5.3"] |
||||
|
||||
# group: |
||||
# 0: ^ > >= < <= (optional) |
||||
# 1: ' ' (optional) |
||||
# 2: version number |
||||
# 3: version number |
||||
# 4: version number |
||||
PATTERN = re.compile('(\^|>|>=|<|<=)?([ ]+)?(\d+)\.(\d+)\.(\d+)') |
||||
|
||||
|
||||
def format(slither, result): |
||||
elements = result['elements'] |
||||
versions_used = [] |
||||
for element in elements: |
||||
versions_used.append(''.join(element['type_specific_fields']['directive'][1:])) |
||||
solc_version_replace = _analyse_versions(versions_used) |
||||
for element in elements: |
||||
_patch(slither, result, element['source_mapping']['filename_absolute'], solc_version_replace, |
||||
element['source_mapping']['start'], |
||||
element['source_mapping']['start'] + element['source_mapping']['length']) |
||||
|
||||
|
||||
def _analyse_versions(used_solc_versions): |
||||
replace_solc_versions = list() |
||||
for version in used_solc_versions: |
||||
replace_solc_versions.append(_determine_solc_version_replacement(version)) |
||||
if not all(version == replace_solc_versions[0] for version in replace_solc_versions): |
||||
raise FormatImpossible("Multiple incompatible versions!") |
||||
else: |
||||
return replace_solc_versions[0] |
||||
|
||||
|
||||
def _determine_solc_version_replacement(used_solc_version): |
||||
versions = PATTERN.findall(used_solc_version) |
||||
if len(versions) == 1: |
||||
version = versions[0] |
||||
minor_version = '.'.join(version[2:])[2] |
||||
if minor_version == '4': |
||||
return "pragma solidity " + REPLACEMENT_VERSIONS[0] + ';' |
||||
elif minor_version == '5': |
||||
return "pragma solidity " + REPLACEMENT_VERSIONS[1] + ';' |
||||
else: |
||||
raise FormatImpossible("Unknown version!") |
||||
elif len(versions) == 2: |
||||
version_right = versions[1] |
||||
minor_version_right = '.'.join(version_right[2:])[2] |
||||
if minor_version_right == '4': |
||||
# Replace with 0.4.25 |
||||
return "pragma solidity " + REPLACEMENT_VERSIONS[0] + ';' |
||||
elif minor_version_right in ['5', '6']: |
||||
# Replace with 0.5.3 |
||||
return "pragma solidity " + REPLACEMENT_VERSIONS[1] + ';' |
||||
|
||||
|
||||
def _patch(slither, result, in_file, pragma, modify_loc_start, modify_loc_end): |
||||
in_file_str = slither.source_code[in_file].encode('utf8') |
||||
old_str_of_interest = in_file_str[modify_loc_start:modify_loc_end] |
||||
create_patch(result, |
||||
in_file, |
||||
int(modify_loc_start), |
||||
int(modify_loc_end), |
||||
old_str_of_interest, |
||||
pragma) |
@ -0,0 +1,59 @@ |
||||
import re |
||||
from slither.formatters.exceptions import FormatImpossible |
||||
from slither.formatters.utils.patches import create_patch |
||||
|
||||
|
||||
# Indicates the recommended versions for replacement |
||||
REPLACEMENT_VERSIONS = ["^0.4.25", "^0.5.3"] |
||||
|
||||
# group: |
||||
# 0: ^ > >= < <= (optional) |
||||
# 1: ' ' (optional) |
||||
# 2: version number |
||||
# 3: version number |
||||
# 4: version number |
||||
PATTERN = re.compile('(\^|>|>=|<|<=)?([ ]+)?(\d+)\.(\d+)\.(\d+)') |
||||
|
||||
def format(slither, result): |
||||
elements = result['elements'] |
||||
for element in elements: |
||||
solc_version_replace = _determine_solc_version_replacement( |
||||
''.join(element['type_specific_fields']['directive'][1:])) |
||||
|
||||
_patch(slither, result, element['source_mapping']['filename_absolute'], solc_version_replace, |
||||
element['source_mapping']['start'], element['source_mapping']['start'] + |
||||
element['source_mapping']['length']) |
||||
|
||||
def _determine_solc_version_replacement(used_solc_version): |
||||
versions = PATTERN.findall(used_solc_version) |
||||
if len(versions) == 1: |
||||
version = versions[0] |
||||
minor_version = '.'.join(version[2:])[2] |
||||
if minor_version == '4': |
||||
# Replace with 0.4.25 |
||||
return "pragma solidity " + REPLACEMENT_VERSIONS[0] + ';' |
||||
elif minor_version == '5': |
||||
# Replace with 0.5.3 |
||||
return "pragma solidity " + REPLACEMENT_VERSIONS[1] + ';' |
||||
else: |
||||
raise FormatImpossible(f"Unknown version {versions}") |
||||
elif len(versions) == 2: |
||||
version_right = versions[1] |
||||
minor_version_right = '.'.join(version_right[2:])[2] |
||||
if minor_version_right == '4': |
||||
# Replace with 0.4.25 |
||||
return "pragma solidity " + REPLACEMENT_VERSIONS[0] + ';' |
||||
elif minor_version_right in ['5','6']: |
||||
# Replace with 0.5.3 |
||||
return "pragma solidity " + REPLACEMENT_VERSIONS[1] + ';' |
||||
|
||||
|
||||
def _patch(slither, result, in_file, solc_version, modify_loc_start, modify_loc_end): |
||||
in_file_str = slither.source_code[in_file].encode('utf8') |
||||
old_str_of_interest = in_file_str[modify_loc_start:modify_loc_end] |
||||
create_patch(result, |
||||
in_file, |
||||
int(modify_loc_start), |
||||
int(modify_loc_end), |
||||
old_str_of_interest, |
||||
solc_version) |
@ -0,0 +1,5 @@ |
||||
from slither.exceptions import SlitherException |
||||
|
||||
class FormatImpossible(SlitherException): pass |
||||
|
||||
class FormatError(SlitherException): pass |
@ -0,0 +1,42 @@ |
||||
import re |
||||
from slither.formatters.utils.patches import create_patch |
||||
|
||||
def format(slither, result): |
||||
elements = result['elements'] |
||||
for element in elements: |
||||
target_contract = slither.get_contract_from_name(element['type_specific_fields']['parent']['name']) |
||||
if target_contract: |
||||
function = target_contract.get_function_from_signature(element['type_specific_fields']['signature']) |
||||
if function: |
||||
_patch(slither, |
||||
result, |
||||
element['source_mapping']['filename_absolute'], |
||||
int(function.parameters_src.source_mapping['start']), |
||||
int(function.returns_src.source_mapping['start'])) |
||||
|
||||
|
||||
def _patch(slither, result, in_file, modify_loc_start, modify_loc_end): |
||||
in_file_str = slither.source_code[in_file].encode('utf8') |
||||
old_str_of_interest = in_file_str[modify_loc_start:modify_loc_end] |
||||
# Search for 'public' keyword which is in-between the function name and modifier name (if present) |
||||
# regex: 'public' could have spaces around or be at the end of the line |
||||
m = re.search(r'((\spublic)\s+)|(\spublic)$|(\)public)$', old_str_of_interest.decode('utf-8')) |
||||
if m is None: |
||||
# No visibility specifier exists; public by default. |
||||
create_patch(result, |
||||
in_file, |
||||
# start after the function definition's closing paranthesis |
||||
modify_loc_start + len(old_str_of_interest.decode('utf-8').split(')')[0]) + 1, |
||||
# end is same as start because we insert the keyword `external` at that location |
||||
modify_loc_start + len(old_str_of_interest.decode('utf-8').split(')')[0]) + 1, |
||||
"", |
||||
" external") # replace_text is `external` |
||||
else: |
||||
create_patch(result, |
||||
in_file, |
||||
# start at the keyword `public` |
||||
modify_loc_start + m.span()[0] + 1, |
||||
# end after the keyword `public` = start + len('public'') |
||||
modify_loc_start + m.span()[0] + 1 + len('public'), |
||||
"public", |
||||
"external") |
@ -0,0 +1,609 @@ |
||||
import re |
||||
import logging |
||||
from slither.slithir.operations import Send, Transfer, OperationWithLValue, HighLevelCall, LowLevelCall, \ |
||||
InternalCall, InternalDynamicCall |
||||
from slither.core.declarations import Modifier |
||||
from slither.core.solidity_types import UserDefinedType, MappingType |
||||
from slither.core.declarations import Enum, Contract, Structure, Function |
||||
from slither.core.solidity_types.elementary_type import ElementaryTypeName |
||||
from slither.core.variables.local_variable import LocalVariable |
||||
from slither.formatters.exceptions import FormatError, FormatImpossible |
||||
from slither.formatters.utils.patches import create_patch |
||||
|
||||
logging.basicConfig(level=logging.INFO) |
||||
logger = logging.getLogger('Slither.Format') |
||||
|
||||
def format(slither, result): |
||||
elements = result['elements'] |
||||
for element in elements: |
||||
target = element['additional_fields']['target'] |
||||
|
||||
convention = element['additional_fields']['convention'] |
||||
|
||||
if convention == "l_O_I_should_not_be_used": |
||||
# l_O_I_should_not_be_used cannot be automatically patched |
||||
logger.info(f'The following naming convention cannot be patched: \n{result["description"]}') |
||||
continue |
||||
|
||||
_patch(slither, result, element, target) |
||||
|
||||
# endregion |
||||
################################################################################### |
||||
################################################################################### |
||||
# region Conventions |
||||
################################################################################### |
||||
################################################################################### |
||||
|
||||
KEY = 'ALL_NAMES_USED' |
||||
|
||||
# https://solidity.readthedocs.io/en/v0.5.11/miscellaneous.html#reserved-keywords |
||||
SOLIDITY_KEYWORDS = ['abstract', 'after', 'alias', 'apply', 'auto', 'case', 'catch', 'copyof', 'default', 'define', |
||||
'final', 'immutable', 'implements', 'in', 'inline', 'let', 'macro', 'match', 'mutable', 'null', |
||||
'of', 'override', 'partial', 'promise', 'reference', 'relocatable', 'sealed', 'sizeof', 'static', |
||||
'supports', 'switch', 'try', 'typedef', 'typeof', 'unchecked'] |
||||
|
||||
# https://solidity.readthedocs.io/en/v0.5.11/miscellaneous.html#language-grammar |
||||
SOLIDITY_KEYWORDS += ['pragma', 'import', 'contract', 'library', 'contract', 'function', 'using', 'struct', 'enum', |
||||
'public', 'private', 'internal', 'external', 'calldata', 'memory', 'modifier', 'view', 'pure', |
||||
'constant', 'storage', 'for', 'if', 'while', 'break', 'return', 'throw', 'else', 'type'] |
||||
|
||||
SOLIDITY_KEYWORDS += ElementaryTypeName |
||||
|
||||
def _name_already_use(slither, name): |
||||
# Do not convert to a name used somewhere else |
||||
if not KEY in slither.context: |
||||
all_names = set() |
||||
for contract in slither.contracts_derived: |
||||
all_names = all_names.union(set([st.name for st in contract.structures])) |
||||
all_names = all_names.union(set([f.name for f in contract.functions_and_modifiers])) |
||||
all_names = all_names.union(set([e.name for e in contract.enums])) |
||||
all_names = all_names.union(set([s.name for s in contract.state_variables])) |
||||
|
||||
for function in contract.functions: |
||||
all_names = all_names.union(set([v.name for v in function.variables])) |
||||
|
||||
slither.context[KEY] = all_names |
||||
return name in slither.context[KEY] |
||||
|
||||
def _convert_CapWords(original_name, slither): |
||||
name = original_name.capitalize() |
||||
|
||||
while '_' in name: |
||||
offset = name.find('_') |
||||
if len(name) > offset: |
||||
name = name[0:offset] + name[offset+1].upper() + name[offset+1:] |
||||
|
||||
if _name_already_use(slither, name): |
||||
raise FormatImpossible(f'{original_name} cannot be converted to {name} (already used)') |
||||
|
||||
if name in SOLIDITY_KEYWORDS: |
||||
raise FormatImpossible(f'{original_name} cannot be converted to {name} (Solidity keyword)') |
||||
return name |
||||
|
||||
def _convert_mixedCase(original_name, slither): |
||||
name = original_name |
||||
if isinstance(name, bytes): |
||||
name = name.decode('utf8') |
||||
|
||||
while '_' in name: |
||||
offset = name.find('_') |
||||
if len(name) > offset: |
||||
name = name[0:offset] + name[offset + 1].upper() + name[offset + 2:] |
||||
|
||||
name = name[0].lower() + name[1:] |
||||
if _name_already_use(slither, name): |
||||
raise FormatImpossible(f'{original_name} cannot be converted to {name} (already used)') |
||||
if name in SOLIDITY_KEYWORDS: |
||||
raise FormatImpossible(f'{original_name} cannot be converted to {name} (Solidity keyword)') |
||||
return name |
||||
|
||||
def _convert_UPPER_CASE_WITH_UNDERSCORES(name, slither): |
||||
if _name_already_use(slither, name.upper()): |
||||
raise FormatImpossible(f'{name} cannot be converted to {name.upper()} (already used)') |
||||
if name.upper() in SOLIDITY_KEYWORDS: |
||||
raise FormatImpossible(f'{name} cannot be converted to {name.upper()} (Solidity keyword)') |
||||
return name.upper() |
||||
|
||||
conventions ={ |
||||
"CapWords":_convert_CapWords, |
||||
"mixedCase":_convert_mixedCase, |
||||
"UPPER_CASE_WITH_UNDERSCORES":_convert_UPPER_CASE_WITH_UNDERSCORES |
||||
} |
||||
|
||||
|
||||
# endregion |
||||
################################################################################### |
||||
################################################################################### |
||||
# region Helpers |
||||
################################################################################### |
||||
################################################################################### |
||||
|
||||
def _get_from_contract(slither, element, name, getter): |
||||
contract_name = element['type_specific_fields']['parent']['name'] |
||||
contract = slither.get_contract_from_name(contract_name) |
||||
return getattr(contract, getter)(name) |
||||
|
||||
# endregion |
||||
################################################################################### |
||||
################################################################################### |
||||
# region Patch dispatcher |
||||
################################################################################### |
||||
################################################################################### |
||||
|
||||
def _patch(slither, result, element, _target): |
||||
|
||||
if _target == "contract": |
||||
target = slither.get_contract_from_name(element['name']) |
||||
|
||||
elif _target == "structure": |
||||
target = _get_from_contract(slither, element, element['name'], 'get_structure_from_name') |
||||
|
||||
elif _target == "event": |
||||
target = _get_from_contract(slither, element, element['name'], 'get_event_from_name') |
||||
|
||||
elif _target == "function": |
||||
# Avoid constructor (FP?) |
||||
if element['name'] != element['type_specific_fields']['parent']['name']: |
||||
function_sig = element['type_specific_fields']['signature'] |
||||
target = _get_from_contract(slither, element, function_sig, 'get_function_from_signature') |
||||
|
||||
elif _target == "modifier": |
||||
modifier_sig = element['type_specific_fields']['signature'] |
||||
target = _get_from_contract(slither, element, modifier_sig, 'get_modifier_from_signature') |
||||
|
||||
elif _target == "parameter": |
||||
contract_name = element['type_specific_fields']['parent']['type_specific_fields']['parent']['name'] |
||||
function_sig = element['type_specific_fields']['parent']['type_specific_fields']['signature'] |
||||
param_name = element['name'] |
||||
contract = slither.get_contract_from_name(contract_name) |
||||
function = contract.get_function_from_signature(function_sig) |
||||
target = function.get_local_variable_from_name(param_name) |
||||
|
||||
elif _target in ["variable", "variable_constant"]: |
||||
# Local variable |
||||
if element['type_specific_fields']['parent'] == 'function': |
||||
contract_name = element['type_specific_fields']['parent']['type_specific_fields']['parent']['name'] |
||||
function_sig = element['type_specific_fields']['parent']['type_specific_fields']['signature'] |
||||
var_name = element['name'] |
||||
contract = slither.get_contract_from_name(contract_name) |
||||
function = contract.get_function_from_signature(function_sig) |
||||
target = function.get_local_variable_from_name(var_name) |
||||
# State variable |
||||
else: |
||||
target = _get_from_contract(slither, element, element['name'], 'get_state_variable_from_name') |
||||
|
||||
elif _target == "enum": |
||||
target = _get_from_contract(slither, element, element['name'], 'get_enum_from_canonical_name') |
||||
|
||||
else: |
||||
raise FormatError("Unknown naming convention! " + _target) |
||||
|
||||
_explore(slither, |
||||
result, |
||||
target, |
||||
conventions[element['additional_fields']['convention']]) |
||||
|
||||
|
||||
# endregion |
||||
################################################################################### |
||||
################################################################################### |
||||
# region Explore functions |
||||
################################################################################### |
||||
################################################################################### |
||||
|
||||
# group 1: beginning of the from type |
||||
# group 2: beginning of the to type |
||||
# nested mapping are within the group 1 |
||||
#RE_MAPPING = '[ ]*mapping[ ]*\([ ]*([\=\>\(\) a-zA-Z0-9\._\[\]]*)[ ]*=>[ ]*([a-zA-Z0-9\._\[\]]*)\)' |
||||
RE_MAPPING_FROM = b'([a-zA-Z0-9\._\[\]]*)' |
||||
RE_MAPPING_TO = b'([\=\>\(\) a-zA-Z0-9\._\[\]\ ]*)' |
||||
RE_MAPPING = b'[ ]*mapping[ ]*\([ ]*' + RE_MAPPING_FROM + b'[ ]*' + b'=>' + b'[ ]*'+ RE_MAPPING_TO + b'\)' |
||||
|
||||
|
||||
def _is_var_declaration(slither, filename, start): |
||||
''' |
||||
Detect usage of 'var ' for Solidity < 0.5 |
||||
:param slither: |
||||
:param filename: |
||||
:param start: |
||||
:return: |
||||
''' |
||||
v = 'var ' |
||||
return slither.source_code[filename][start:start + len(v)] == v |
||||
|
||||
|
||||
def _explore_type(slither, result, target, convert, type, filename_source_code, start, end): |
||||
if isinstance(type, UserDefinedType): |
||||
# Patch type based on contract/enum |
||||
if isinstance(type.type, (Enum, Contract)): |
||||
if type.type == target: |
||||
old_str = type.type.name |
||||
new_str = convert(old_str, slither) |
||||
|
||||
loc_start = start |
||||
if _is_var_declaration(slither, filename_source_code, start): |
||||
loc_end = loc_start + len('var') |
||||
else: |
||||
loc_end = loc_start + len(old_str) |
||||
|
||||
create_patch(result, |
||||
filename_source_code, |
||||
loc_start, |
||||
loc_end, |
||||
old_str, |
||||
new_str) |
||||
|
||||
|
||||
else: |
||||
# Patch type based on structure |
||||
assert isinstance(type.type, Structure) |
||||
if type.type == target: |
||||
old_str = type.type.name |
||||
new_str = convert(old_str, slither) |
||||
|
||||
loc_start = start |
||||
if _is_var_declaration(slither, filename_source_code, start): |
||||
loc_end = loc_start + len('var') |
||||
else: |
||||
loc_end = loc_start + len(old_str) |
||||
|
||||
create_patch(result, |
||||
filename_source_code, |
||||
loc_start, |
||||
loc_end, |
||||
old_str, |
||||
new_str) |
||||
|
||||
# Structure contain a list of elements, that might need patching |
||||
# .elems return a list of VariableStructure |
||||
_explore_variables_declaration(slither, |
||||
type.type.elems.values(), |
||||
result, |
||||
target, |
||||
convert) |
||||
|
||||
if isinstance(type, MappingType): |
||||
# Mapping has three steps: |
||||
# Convert the "from" type |
||||
# Convert the "to" type |
||||
# Convert nested type in the "to" |
||||
# Ex: mapping (mapping (badName => uint) => uint) |
||||
|
||||
# Do the comparison twice, so we can factor together the re matching |
||||
# mapping can only have elementary type in type_from |
||||
if isinstance(type.type_to, (UserDefinedType, MappingType)) or target in [type.type_from, type.type_to]: |
||||
|
||||
full_txt_start = start |
||||
full_txt_end = end |
||||
full_txt = slither.source_code[filename_source_code].encode('utf8')[full_txt_start:full_txt_end] |
||||
re_match = re.match(RE_MAPPING, full_txt) |
||||
assert re_match |
||||
|
||||
if type.type_from == target: |
||||
old_str = type.type_from.name |
||||
new_str = convert(old_str, slither) |
||||
|
||||
loc_start = start + re_match.start(1) |
||||
loc_end = loc_start + len(old_str) |
||||
|
||||
create_patch(result, |
||||
filename_source_code, |
||||
loc_start, |
||||
loc_end, |
||||
old_str, |
||||
new_str) |
||||
|
||||
if type.type_to == target: |
||||
|
||||
old_str = type.type_to.name |
||||
new_str = convert(old_str, slither) |
||||
|
||||
loc_start = start + re_match.start(2) |
||||
loc_end = loc_start + len(old_str) |
||||
|
||||
create_patch(result, |
||||
filename_source_code, |
||||
loc_start, |
||||
loc_end, |
||||
old_str, |
||||
new_str) |
||||
|
||||
if isinstance(type.type_to, (UserDefinedType, MappingType)): |
||||
loc_start = start + re_match.start(2) |
||||
loc_end = start + re_match.end(2) |
||||
_explore_type(slither, |
||||
result, |
||||
target, |
||||
convert, |
||||
type.type_to, |
||||
filename_source_code, |
||||
loc_start, |
||||
loc_end) |
||||
|
||||
|
||||
|
||||
def _explore_variables_declaration(slither, variables, result, target, convert, patch_comment=False): |
||||
for variable in variables: |
||||
# First explore the type of the variable |
||||
filename_source_code = variable.source_mapping['filename_absolute'] |
||||
full_txt_start = variable.source_mapping['start'] |
||||
full_txt_end = full_txt_start + variable.source_mapping['length'] |
||||
full_txt = slither.source_code[filename_source_code].encode('utf8')[full_txt_start:full_txt_end] |
||||
|
||||
_explore_type(slither, |
||||
result, |
||||
target, |
||||
convert, |
||||
variable.type, |
||||
filename_source_code, |
||||
full_txt_start, |
||||
variable.source_mapping['start'] + variable.source_mapping['length']) |
||||
|
||||
# If the variable is the target |
||||
if variable == target: |
||||
old_str = variable.name |
||||
new_str = convert(old_str, slither) |
||||
|
||||
loc_start = full_txt_start + full_txt.find(old_str.encode('utf8')) |
||||
loc_end = loc_start + len(old_str) |
||||
|
||||
create_patch(result, |
||||
filename_source_code, |
||||
loc_start, |
||||
loc_end, |
||||
old_str, |
||||
new_str) |
||||
|
||||
# Patch comment only makes sense for local variable declaration in the parameter list |
||||
if patch_comment and isinstance(variable, LocalVariable): |
||||
if 'lines' in variable.source_mapping and variable.source_mapping['lines']: |
||||
func = variable.function |
||||
end_line = func.source_mapping['lines'][0] |
||||
if variable in func.parameters: |
||||
idx = len(func.parameters) - func.parameters.index(variable) + 1 |
||||
first_line = end_line - idx - 2 |
||||
|
||||
potential_comments = slither.source_code[filename_source_code].encode('utf8') |
||||
potential_comments = potential_comments.splitlines(keepends=True)[first_line:end_line-1] |
||||
|
||||
idx_beginning = func.source_mapping['start'] |
||||
idx_beginning += - func.source_mapping['starting_column'] + 1 |
||||
idx_beginning += - sum([len(c) for c in potential_comments]) |
||||
|
||||
old_comment = f'@param {old_str}'.encode('utf8') |
||||
|
||||
for line in potential_comments: |
||||
idx = line.find(old_comment) |
||||
if idx >=0: |
||||
loc_start = idx + idx_beginning |
||||
loc_end = loc_start + len(old_comment) |
||||
new_comment = f'@param {new_str}'.encode('utf8') |
||||
|
||||
create_patch(result, |
||||
filename_source_code, |
||||
loc_start, |
||||
loc_end, |
||||
old_comment, |
||||
new_comment) |
||||
|
||||
break |
||||
idx_beginning += len(line) |
||||
|
||||
|
||||
|
||||
|
||||
def _explore_modifiers_calls(slither, function, result, target, convert): |
||||
for modifier in function.modifiers_statements: |
||||
for node in modifier.nodes: |
||||
if node.irs: |
||||
_explore_irs(slither, node.irs, result, target, convert) |
||||
for modifier in function.explicit_base_constructor_calls_statements: |
||||
for node in modifier.nodes: |
||||
if node.irs: |
||||
_explore_irs(slither, node.irs, result, target, convert) |
||||
|
||||
def _explore_structures_declaration(slither, structures, result, target, convert): |
||||
for st in structures: |
||||
# Explore the variable declared within the structure (VariableStructure) |
||||
_explore_variables_declaration(slither, st.elems.values(), result, target, convert) |
||||
|
||||
# If the structure is the target |
||||
if st == target: |
||||
old_str = st.name |
||||
new_str = convert(old_str, slither) |
||||
|
||||
filename_source_code = st.source_mapping['filename_absolute'] |
||||
full_txt_start = st.source_mapping['start'] |
||||
full_txt_end = full_txt_start + st.source_mapping['length'] |
||||
full_txt = slither.source_code[filename_source_code].encode('utf8')[full_txt_start:full_txt_end] |
||||
|
||||
# The name is after the space |
||||
matches = re.finditer(b'struct[ ]*', full_txt) |
||||
# Look for the end offset of the largest list of ' ' |
||||
loc_start = full_txt_start + max(matches, key=lambda x: len(x.group())).end() |
||||
loc_end = loc_start + len(old_str) |
||||
|
||||
create_patch(result, |
||||
filename_source_code, |
||||
loc_start, |
||||
loc_end, |
||||
old_str, |
||||
new_str) |
||||
|
||||
|
||||
def _explore_events_declaration(slither, events, result, target, convert): |
||||
for event in events: |
||||
# Explore the parameters |
||||
_explore_variables_declaration(slither, event.elems, result, target, convert) |
||||
|
||||
# If the event is the target |
||||
if event == target: |
||||
filename_source_code = event.source_mapping['filename_absolute'] |
||||
|
||||
old_str = event.name |
||||
new_str = convert(old_str, slither) |
||||
|
||||
loc_start = event.source_mapping['start'] |
||||
loc_end = loc_start + len(old_str) |
||||
|
||||
create_patch(result, |
||||
filename_source_code, |
||||
loc_start, |
||||
loc_end, |
||||
old_str, |
||||
new_str) |
||||
|
||||
def get_ir_variables(ir): |
||||
vars = ir.read |
||||
|
||||
if isinstance(ir, (InternalCall, InternalDynamicCall, HighLevelCall)): |
||||
vars += [ir.function] |
||||
|
||||
if isinstance(ir, (HighLevelCall, Send, LowLevelCall, Transfer)): |
||||
vars += [ir.call_value] |
||||
|
||||
if isinstance(ir, (HighLevelCall, LowLevelCall)): |
||||
vars += [ir.call_gas] |
||||
|
||||
if isinstance(ir, OperationWithLValue): |
||||
vars += [ir.lvalue] |
||||
|
||||
return [v for v in vars if v] |
||||
|
||||
def _explore_irs(slither, irs, result, target, convert): |
||||
if irs is None: |
||||
return |
||||
for ir in irs: |
||||
for v in get_ir_variables(ir): |
||||
if target == v or ( |
||||
isinstance(target, Function) and isinstance(v, Function) and |
||||
v.canonical_name == target.canonical_name): |
||||
source_mapping = ir.expression.source_mapping |
||||
filename_source_code = source_mapping['filename_absolute'] |
||||
full_txt_start = source_mapping['start'] |
||||
full_txt_end = full_txt_start + source_mapping['length'] |
||||
full_txt = slither.source_code[filename_source_code].encode('utf8')[full_txt_start:full_txt_end] |
||||
|
||||
if not target.name.encode('utf8') in full_txt: |
||||
raise FormatError(f'{target} not found in {full_txt} ({source_mapping}') |
||||
|
||||
old_str = target.name.encode('utf8') |
||||
new_str = convert(old_str, slither) |
||||
|
||||
counter = 0 |
||||
# Can be found multiple time on the same IR |
||||
# We patch one by one |
||||
while old_str in full_txt: |
||||
|
||||
target_found_at = full_txt.find((old_str)) |
||||
|
||||
full_txt = full_txt[target_found_at+1:] |
||||
counter += target_found_at |
||||
|
||||
loc_start = full_txt_start + counter |
||||
loc_end = loc_start + len(old_str) |
||||
|
||||
create_patch(result, |
||||
filename_source_code, |
||||
loc_start, |
||||
loc_end, |
||||
old_str, |
||||
new_str) |
||||
|
||||
|
||||
def _explore_functions(slither, functions, result, target, convert): |
||||
for function in functions: |
||||
_explore_variables_declaration(slither, function.variables, result, target, convert, True) |
||||
_explore_modifiers_calls(slither, function, result, target, convert) |
||||
_explore_irs(slither, function.all_slithir_operations(), result, target, convert) |
||||
|
||||
if isinstance(target, Function) and function.canonical_name == target.canonical_name: |
||||
old_str = function.name |
||||
new_str = convert(old_str, slither) |
||||
|
||||
filename_source_code = function.source_mapping['filename_absolute'] |
||||
full_txt_start = function.source_mapping['start'] |
||||
full_txt_end = full_txt_start + function.source_mapping['length'] |
||||
full_txt = slither.source_code[filename_source_code].encode('utf8')[full_txt_start:full_txt_end] |
||||
|
||||
# The name is after the space |
||||
if isinstance(target, Modifier): |
||||
matches = re.finditer(b'modifier([ ]*)', full_txt) |
||||
else: |
||||
matches = re.finditer(b'function([ ]*)', full_txt) |
||||
# Look for the end offset of the largest list of ' ' |
||||
loc_start = full_txt_start + max(matches, key=lambda x: len(x.group())).end() |
||||
loc_end = loc_start + len(old_str) |
||||
|
||||
create_patch(result, |
||||
filename_source_code, |
||||
loc_start, |
||||
loc_end, |
||||
old_str, |
||||
new_str) |
||||
|
||||
def _explore_enums(slither, enums, result, target, convert): |
||||
for enum in enums: |
||||
if enum == target: |
||||
old_str = enum.name |
||||
new_str = convert(old_str, slither) |
||||
|
||||
filename_source_code = enum.source_mapping['filename_absolute'] |
||||
full_txt_start = enum.source_mapping['start'] |
||||
full_txt_end = full_txt_start + enum.source_mapping['length'] |
||||
full_txt = slither.source_code[filename_source_code].encode('utf8')[full_txt_start:full_txt_end] |
||||
|
||||
# The name is after the space |
||||
matches = re.finditer(b'enum([ ]*)', full_txt) |
||||
# Look for the end offset of the largest list of ' ' |
||||
loc_start = full_txt_start + max(matches, key=lambda x: len(x.group())).end() |
||||
loc_end = loc_start + len(old_str) |
||||
|
||||
create_patch(result, |
||||
filename_source_code, |
||||
loc_start, |
||||
loc_end, |
||||
old_str, |
||||
new_str) |
||||
|
||||
|
||||
def _explore_contract(slither, contract, result, target, convert): |
||||
_explore_variables_declaration(slither, contract.state_variables, result, target, convert) |
||||
_explore_structures_declaration(slither, contract.structures, result, target, convert) |
||||
_explore_functions(slither, contract.functions_and_modifiers, result, target, convert) |
||||
_explore_enums(slither, contract.enums, result, target, convert) |
||||
|
||||
if contract == target: |
||||
filename_source_code = contract.source_mapping['filename_absolute'] |
||||
full_txt_start = contract.source_mapping['start'] |
||||
full_txt_end = full_txt_start + contract.source_mapping['length'] |
||||
full_txt = slither.source_code[filename_source_code].encode('utf8')[full_txt_start:full_txt_end] |
||||
|
||||
old_str = contract.name |
||||
new_str = convert(old_str, slither) |
||||
|
||||
# The name is after the space |
||||
matches = re.finditer(b'contract[ ]*', full_txt) |
||||
# Look for the end offset of the largest list of ' ' |
||||
loc_start = full_txt_start + max(matches, key=lambda x: len(x.group())).end() |
||||
|
||||
loc_end = loc_start + len(old_str) |
||||
|
||||
create_patch(result, |
||||
filename_source_code, |
||||
loc_start, |
||||
loc_end, |
||||
old_str, |
||||
new_str) |
||||
|
||||
|
||||
def _explore(slither, result, target, convert): |
||||
for contract in slither.contracts_derived: |
||||
_explore_contract(slither, contract, result, target, convert) |
||||
|
||||
|
||||
|
||||
|
||||
# endregion |
||||
|
||||
|
@ -0,0 +1,43 @@ |
||||
import os |
||||
import difflib |
||||
from collections import defaultdict |
||||
|
||||
def create_patch(result, file, start, end, old_str, new_str): |
||||
if isinstance(old_str, bytes): |
||||
old_str = old_str.decode('utf8') |
||||
if isinstance(new_str, bytes): |
||||
new_str = new_str.decode('utf8') |
||||
p = {"start": start, |
||||
"end": end, |
||||
"old_string": old_str, |
||||
"new_string": new_str |
||||
} |
||||
if 'patches' not in result: |
||||
result['patches'] = defaultdict(list) |
||||
if p not in result['patches'][file]: |
||||
result['patches'][file].append(p) |
||||
|
||||
|
||||
def apply_patch(original_txt, patch, offset): |
||||
patched_txt = original_txt[:int(patch['start'] + offset)] |
||||
patched_txt += patch['new_string'].encode('utf8') |
||||
patched_txt += original_txt[int(patch['end'] + offset):] |
||||
|
||||
# Keep the diff of text added or sub, in case of multiple patches |
||||
patch_length_diff = len(patch['new_string']) - (patch['end'] - patch['start']) |
||||
return patched_txt, patch_length_diff + offset |
||||
|
||||
|
||||
def create_diff(slither, original_txt, patched_txt, filename): |
||||
if slither.crytic_compile: |
||||
relative_path = slither.crytic_compile.filename_lookup(filename).relative |
||||
relative_path = os.path.join('.', relative_path) |
||||
else: |
||||
relative_path = filename |
||||
diff = difflib.unified_diff(original_txt.decode('utf8').splitlines(False), |
||||
patched_txt.decode('utf8').splitlines(False), |
||||
fromfile=relative_path, |
||||
tofile=relative_path, |
||||
lineterm='') |
||||
|
||||
return '\n'.join(list(diff)) + '\n' |
@ -0,0 +1,38 @@ |
||||
import re |
||||
from slither.formatters.exceptions import FormatError, FormatImpossible |
||||
from slither.formatters.utils.patches import create_patch |
||||
|
||||
def format(slither, result): |
||||
elements = result['elements'] |
||||
for element in elements: |
||||
|
||||
# TODO: decide if this should be changed in the constant detector |
||||
contract_name = element['type_specific_fields']['parent']['name'] |
||||
contract = slither.get_contract_from_name(contract_name) |
||||
var = contract.get_state_variable_from_name(element['name']) |
||||
if not var.expression: |
||||
raise FormatImpossible(f'{var.name} is uninitialized and cannot become constant.') |
||||
|
||||
_patch(slither, result, element['source_mapping']['filename_absolute'], |
||||
element['name'], |
||||
"constant " + element['name'], |
||||
element['source_mapping']['start'], |
||||
element['source_mapping']['start'] + element['source_mapping']['length']) |
||||
|
||||
|
||||
def _patch(slither, result, in_file, match_text, replace_text, modify_loc_start, modify_loc_end): |
||||
in_file_str = slither.source_code[in_file].encode('utf8') |
||||
old_str_of_interest = in_file_str[modify_loc_start:modify_loc_end] |
||||
# Add keyword `constant` before the variable name |
||||
(new_str_of_interest, num_repl) = re.subn(match_text, replace_text, old_str_of_interest.decode('utf-8'), 1) |
||||
if num_repl != 0: |
||||
create_patch(result, |
||||
in_file, |
||||
modify_loc_start, |
||||
modify_loc_end, |
||||
old_str_of_interest, |
||||
new_str_of_interest) |
||||
|
||||
else: |
||||
raise FormatError("State variable not found?!") |
||||
|
@ -0,0 +1,28 @@ |
||||
from slither.formatters.utils.patches import create_patch |
||||
|
||||
|
||||
def format(slither, result): |
||||
elements = result['elements'] |
||||
for element in elements: |
||||
if element['type'] == "variable": |
||||
_patch(slither, |
||||
result, |
||||
element['source_mapping']['filename_absolute'], |
||||
element['source_mapping']['start']) |
||||
|
||||
|
||||
def _patch(slither, result, in_file, modify_loc_start): |
||||
in_file_str = slither.source_code[in_file].encode('utf8') |
||||
old_str_of_interest = in_file_str[modify_loc_start:] |
||||
old_str = old_str_of_interest.decode('utf-8').partition(';')[0]\ |
||||
+ old_str_of_interest.decode('utf-8').partition(';')[1] |
||||
|
||||
create_patch(result, |
||||
in_file, |
||||
int(modify_loc_start), |
||||
# Remove the entire declaration until the semicolon |
||||
int(modify_loc_start + len(old_str_of_interest.decode('utf-8').partition(';')[0]) + 1), |
||||
old_str, |
||||
"") |
||||
|
||||
|
@ -0,0 +1,14 @@ |
||||
# .format files are the output files produced by slither-format |
||||
# .patch files are the output files produced by slither-format |
||||
*.format |
||||
*.patch |
||||
|
||||
# Temporary files (Emacs backup files ending in tilde and others) |
||||
*~ |
||||
*.err |
||||
*.out |
||||
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,89 @@ |
||||
import sys |
||||
import argparse |
||||
from slither import Slither |
||||
from slither.utils.command_line import read_config_file |
||||
import logging |
||||
from .slither_format import slither_format |
||||
from crytic_compile import cryticparser |
||||
|
||||
logging.basicConfig() |
||||
logger = logging.getLogger("Slither").setLevel(logging.INFO) |
||||
|
||||
# Slither detectors for which slither-format currently works |
||||
available_detectors = ["unused-state", |
||||
"solc-version", |
||||
"pragma", |
||||
"naming-convention", |
||||
"external-function", |
||||
"constable-states", |
||||
"constant-function"] |
||||
|
||||
detectors_to_run = [] |
||||
|
||||
def parse_args(): |
||||
""" |
||||
Parse the underlying arguments for the program. |
||||
:return: Returns the arguments for the program. |
||||
""" |
||||
parser = argparse.ArgumentParser(description='slither_format', |
||||
usage='slither_format filename') |
||||
|
||||
parser.add_argument('filename', help='The filename of the contract or truffle directory to analyze.') |
||||
parser.add_argument('--verbose-test', '-v', help='verbose mode output for testing',action='store_true',default=False) |
||||
parser.add_argument('--verbose-json', '-j', help='verbose json output',action='store_true',default=False) |
||||
parser.add_argument('--version', |
||||
help='displays the current version', |
||||
version='0.1.0', |
||||
action='version') |
||||
|
||||
parser.add_argument('--config-file', |
||||
help='Provide a config file (default: slither.config.json)', |
||||
action='store', |
||||
dest='config_file', |
||||
default='slither.config.json') |
||||
|
||||
|
||||
group_detector = parser.add_argument_group('Detectors') |
||||
group_detector.add_argument('--detect', |
||||
help='Comma-separated list of detectors, defaults to all, ' |
||||
'available detectors: {}'.format( |
||||
', '.join(d for d in available_detectors)), |
||||
action='store', |
||||
dest='detectors_to_run', |
||||
default='all') |
||||
|
||||
group_detector.add_argument('--exclude', |
||||
help='Comma-separated list of detectors to exclude,' |
||||
'available detectors: {}'.format( |
||||
', '.join(d for d in available_detectors)), |
||||
action='store', |
||||
dest='detectors_to_exclude', |
||||
default='all') |
||||
|
||||
cryticparser.init(parser) |
||||
|
||||
if len(sys.argv) == 1: |
||||
parser.print_help(sys.stderr) |
||||
sys.exit(1) |
||||
|
||||
return parser.parse_args() |
||||
|
||||
|
||||
def main(): |
||||
# ------------------------------ |
||||
# Usage: python3 -m slither_format filename |
||||
# Example: python3 -m slither_format contract.sol |
||||
# ------------------------------ |
||||
# Parse all arguments |
||||
args = parse_args() |
||||
|
||||
read_config_file(args) |
||||
|
||||
|
||||
# Perform slither analysis on the given filename |
||||
slither = Slither(args.filename, **vars(args)) |
||||
|
||||
# Format the input files based on slither analysis |
||||
slither_format(slither, **vars(args)) |
||||
if __name__ == '__main__': |
||||
main() |
@ -0,0 +1,151 @@ |
||||
import logging |
||||
from pathlib import Path |
||||
from slither.detectors.variables.unused_state_variables import UnusedStateVars |
||||
from slither.detectors.attributes.incorrect_solc import IncorrectSolc |
||||
from slither.detectors.attributes.constant_pragma import ConstantPragma |
||||
from slither.detectors.naming_convention.naming_convention import NamingConvention |
||||
from slither.detectors.functions.external_function import ExternalFunction |
||||
from slither.detectors.variables.possible_const_state_variables import ConstCandidateStateVars |
||||
from slither.detectors.attributes.const_functions import ConstantFunctions |
||||
from slither.utils.colors import yellow |
||||
|
||||
logging.basicConfig(level=logging.INFO) |
||||
logger = logging.getLogger('Slither.Format') |
||||
|
||||
all_detectors = { |
||||
'unused-state': UnusedStateVars, |
||||
'solc-version': IncorrectSolc, |
||||
'pragma': ConstantPragma, |
||||
'naming-convention': NamingConvention, |
||||
'external-function': ExternalFunction, |
||||
'constable-states' : ConstCandidateStateVars, |
||||
'constant-function': ConstantFunctions |
||||
} |
||||
|
||||
def slither_format(slither, **kwargs): |
||||
'''' |
||||
Keyword Args: |
||||
detectors_to_run (str): Comma-separated list of detectors, defaults to all |
||||
''' |
||||
|
||||
detectors_to_run = choose_detectors(kwargs.get('detectors_to_run', 'all'), |
||||
kwargs.get('detectors_to_exclude', '')) |
||||
|
||||
for detector in detectors_to_run: |
||||
slither.register_detector(detector) |
||||
|
||||
slither.generate_patches = True |
||||
|
||||
detector_results = slither.run_detectors() |
||||
detector_results = [x for x in detector_results if x] # remove empty results |
||||
detector_results = [item for sublist in detector_results for item in sublist] # flatten |
||||
|
||||
export = Path('crytic-export', 'patches') |
||||
|
||||
export.mkdir(parents=True, exist_ok=True) |
||||
|
||||
counter_result = 0 |
||||
|
||||
logger.info(yellow('slither-format is in beta, carefully review each patch before merging it.')) |
||||
|
||||
for result in detector_results: |
||||
if not 'patches' in result: |
||||
continue |
||||
one_line_description = result["description"].split("\n")[0] |
||||
|
||||
export_result = Path(export, f'{counter_result}') |
||||
export_result.mkdir(parents=True, exist_ok=True) |
||||
counter_result += 1 |
||||
counter = 0 |
||||
|
||||
logger.info(f'Issue: {one_line_description}') |
||||
logger.info(f'Generated: ({export_result})') |
||||
|
||||
for file, diff, in result['patches_diff'].items(): |
||||
filename = f'fix_{counter}.patch' |
||||
path = Path(export_result, filename) |
||||
logger.info(f'\t- {filename}') |
||||
with open(path, 'w') as f: |
||||
f.write(diff) |
||||
counter += 1 |
||||
|
||||
|
||||
# endregion |
||||
################################################################################### |
||||
################################################################################### |
||||
# region Detectors |
||||
################################################################################### |
||||
################################################################################### |
||||
|
||||
def choose_detectors(detectors_to_run, detectors_to_exclude): |
||||
# If detectors are specified, run only these ones |
||||
cls_detectors_to_run = [] |
||||
exclude = detectors_to_exclude.split(',') |
||||
if detectors_to_run == 'all': |
||||
for d in all_detectors: |
||||
if d in exclude: |
||||
continue |
||||
cls_detectors_to_run.append(all_detectors[d]) |
||||
else: |
||||
exclude = detectors_to_exclude.split(',') |
||||
for d in detectors_to_run.split(','): |
||||
if d in all_detectors: |
||||
if d in exclude: |
||||
continue |
||||
cls_detectors_to_run.append(all_detectors[d]) |
||||
else: |
||||
raise Exception('Error: {} is not a detector'.format(d)) |
||||
return cls_detectors_to_run |
||||
|
||||
# endregion |
||||
################################################################################### |
||||
################################################################################### |
||||
# region Debug functions |
||||
################################################################################### |
||||
################################################################################### |
||||
|
||||
def print_patches(number_of_slither_results, patches): |
||||
logger.info("Number of Slither results: " + str(number_of_slither_results)) |
||||
number_of_patches = 0 |
||||
for file in patches: |
||||
number_of_patches += len(patches[file]) |
||||
logger.info("Number of patches: " + str(number_of_patches)) |
||||
for file in patches: |
||||
logger.info("Patch file: " + file) |
||||
for patch in patches[file]: |
||||
logger.info("Detector: " + patch['detector']) |
||||
logger.info("Old string: " + patch['old_string'].replace("\n","")) |
||||
logger.info("New string: " + patch['new_string'].replace("\n","")) |
||||
logger.info("Location start: " + str(patch['start'])) |
||||
logger.info("Location end: " + str(patch['end'])) |
||||
|
||||
def print_patches_json(number_of_slither_results, patches): |
||||
print('{',end='') |
||||
print("\"Number of Slither results\":" + '"' + str(number_of_slither_results) + '",') |
||||
print("\"Number of patchlets\":" + "\"" + str(len(patches)) + "\"", ',') |
||||
print("\"Patchlets\":" + '[') |
||||
for index, file in enumerate(patches): |
||||
if index > 0: |
||||
print(',') |
||||
print('{',end='') |
||||
print("\"Patch file\":" + '"' + file + '",') |
||||
print("\"Number of patches\":" + "\"" + str(len(patches[file])) + "\"", ',') |
||||
print("\"Patches\":" + '[') |
||||
for index, patch in enumerate(patches[file]): |
||||
if index > 0: |
||||
print(',') |
||||
print('{',end='') |
||||
print("\"Detector\":" + '"' + patch['detector'] + '",') |
||||
print("\"Old string\":" + '"' + patch['old_string'].replace("\n","") + '",') |
||||
print("\"New string\":" + '"' + patch['new_string'].replace("\n","") + '",') |
||||
print("\"Location start\":" + '"' + str(patch['start']) + '",') |
||||
print("\"Location end\":" + '"' + str(patch['end']) + '"') |
||||
if 'overlaps' in patch: |
||||
print("\"Overlaps\":" + "Yes") |
||||
print('}',end='') |
||||
print(']',end='') |
||||
print('}',end='') |
||||
print(']',end='') |
||||
print('}') |
||||
|
||||
|
Loading…
Reference in new issue