certora-cli-beta-mirror 7.28.0__py3-none-any.whl → 8.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- certora_cli/CertoraProver/Compiler/CompilerCollectorFactory.py +10 -3
- certora_cli/CertoraProver/Compiler/CompilerCollectorVy.py +51 -16
- certora_cli/CertoraProver/Compiler/CompilerCollectorYul.py +3 -0
- certora_cli/CertoraProver/castingInstrumenter.py +192 -0
- certora_cli/CertoraProver/certoraApp.py +52 -0
- certora_cli/CertoraProver/certoraBuild.py +694 -207
- certora_cli/CertoraProver/certoraBuildCacheManager.py +21 -17
- certora_cli/CertoraProver/certoraBuildDataClasses.py +8 -2
- certora_cli/CertoraProver/certoraBuildRust.py +88 -54
- certora_cli/CertoraProver/certoraBuildSui.py +112 -0
- certora_cli/CertoraProver/certoraCloudIO.py +97 -96
- certora_cli/CertoraProver/certoraCollectConfigurationLayout.py +230 -84
- certora_cli/CertoraProver/certoraCollectRunMetadata.py +52 -6
- certora_cli/CertoraProver/certoraCompilerParameters.py +11 -0
- certora_cli/CertoraProver/certoraConfigIO.py +43 -35
- certora_cli/CertoraProver/certoraContext.py +128 -54
- certora_cli/CertoraProver/certoraContextAttributes.py +415 -234
- certora_cli/CertoraProver/certoraContextValidator.py +152 -105
- certora_cli/CertoraProver/certoraContractFuncs.py +34 -1
- certora_cli/CertoraProver/certoraParseBuildScript.py +8 -10
- certora_cli/CertoraProver/certoraType.py +10 -1
- certora_cli/CertoraProver/certoraVerifyGenerator.py +22 -4
- certora_cli/CertoraProver/erc7201.py +45 -0
- certora_cli/CertoraProver/splitRules.py +23 -18
- certora_cli/CertoraProver/storageExtension.py +351 -0
- certora_cli/EquivalenceCheck/Eq_default.conf +0 -1
- certora_cli/EquivalenceCheck/Eq_sanity.conf +0 -1
- certora_cli/EquivalenceCheck/equivCheck.py +2 -1
- certora_cli/Mutate/mutateApp.py +41 -22
- certora_cli/Mutate/mutateAttributes.py +11 -0
- certora_cli/Mutate/mutateValidate.py +42 -2
- certora_cli/Shared/certoraAttrUtil.py +21 -5
- certora_cli/Shared/certoraUtils.py +180 -60
- certora_cli/Shared/certoraValidateFuncs.py +68 -26
- certora_cli/Shared/proverCommon.py +308 -0
- certora_cli/certoraCVLFormatter.py +76 -0
- certora_cli/certoraConcord.py +39 -0
- certora_cli/certoraEVMProver.py +4 -3
- certora_cli/certoraRanger.py +39 -0
- certora_cli/certoraRun.py +83 -223
- certora_cli/certoraSolanaProver.py +40 -128
- certora_cli/certoraSorobanProver.py +59 -4
- certora_cli/certoraSuiProver.py +93 -0
- certora_cli_beta_mirror-8.5.0.dist-info/LICENSE +15 -0
- {certora_cli_beta_mirror-7.28.0.dist-info → certora_cli_beta_mirror-8.5.0.dist-info}/METADATA +21 -5
- certora_cli_beta_mirror-8.5.0.dist-info/RECORD +81 -0
- {certora_cli_beta_mirror-7.28.0.dist-info → certora_cli_beta_mirror-8.5.0.dist-info}/WHEEL +1 -1
- {certora_cli_beta_mirror-7.28.0.dist-info → certora_cli_beta_mirror-8.5.0.dist-info}/entry_points.txt +3 -0
- certora_jars/ASTExtraction.jar +0 -0
- certora_jars/CERTORA-CLI-VERSION-METADATA.json +1 -1
- certora_jars/Typechecker.jar +0 -0
- certora_cli_beta_mirror-7.28.0.dist-info/LICENSE +0 -22
- certora_cli_beta_mirror-7.28.0.dist-info/RECORD +0 -70
- {certora_cli_beta_mirror-7.28.0.dist-info → certora_cli_beta_mirror-8.5.0.dist-info}/top_level.txt +0 -0
|
@@ -250,6 +250,17 @@ class MutateAttributes(AttrUtil.Attributes):
|
|
|
250
250
|
}
|
|
251
251
|
)
|
|
252
252
|
|
|
253
|
+
URL_VISIBILITY = MutateAttributeDefinition(
|
|
254
|
+
attr_validation_func=Vf.validate_url_visibility,
|
|
255
|
+
argparse_args={
|
|
256
|
+
'nargs': AttrUtil.SINGLE_OR_NONE_OCCURRENCES,
|
|
257
|
+
'action': AttrUtil.UniqueStore,
|
|
258
|
+
'default': None, # 'default': when --url_visibility was not used
|
|
259
|
+
# when --url_visibility was used without an argument its probably because the link should be public
|
|
260
|
+
'const': str(Vf.UrlVisibilityOptions.PUBLIC)
|
|
261
|
+
}
|
|
262
|
+
)
|
|
263
|
+
|
|
253
264
|
|
|
254
265
|
def get_args(args_list: List[str]) -> Dict:
|
|
255
266
|
|
|
@@ -41,6 +41,46 @@ class MutateValidator:
|
|
|
41
41
|
self.validate_arg_types()
|
|
42
42
|
self.validate_gambit_objs()
|
|
43
43
|
self.validate_attribute_combinations()
|
|
44
|
+
self.validate_manual_mutation()
|
|
45
|
+
|
|
46
|
+
def validate_manual_mutation(self) -> None:
|
|
47
|
+
if self.mutate_app.manual_mutants:
|
|
48
|
+
if not isinstance(self.mutate_app.manual_mutants, list):
|
|
49
|
+
raise Util.CertoraUserInputError("manual_mutants should be a list of objects")
|
|
50
|
+
for mutant in self.mutate_app.manual_mutants:
|
|
51
|
+
if not isinstance(mutant, dict):
|
|
52
|
+
raise Util.CertoraUserInputError(f"manual_mutants should be a list of objects, "
|
|
53
|
+
f"but found {type(mutant)} instead")
|
|
54
|
+
|
|
55
|
+
mandatory_keys = {Constants.FILE_TO_MUTATE, Constants.MUTANTS_LOCATION}
|
|
56
|
+
mutant_keys = set(mutant.keys())
|
|
57
|
+
|
|
58
|
+
missing_keys = mandatory_keys - mutant_keys
|
|
59
|
+
extra_keys = mutant_keys - mandatory_keys
|
|
60
|
+
|
|
61
|
+
if missing_keys:
|
|
62
|
+
raise Util.CertoraUserInputError(f"manual_mutants object must contain keys: {mandatory_keys}, "
|
|
63
|
+
f"missing: {missing_keys}")
|
|
64
|
+
if extra_keys:
|
|
65
|
+
raise Util.CertoraUserInputError(f"manual_mutants object contains invalid keys: {extra_keys}, "
|
|
66
|
+
f"only allowed: {mandatory_keys}")
|
|
67
|
+
try:
|
|
68
|
+
Vf.validate_readable_file(mutant[Constants.FILE_TO_MUTATE], Util.SOL_EXT)
|
|
69
|
+
except Exception as e:
|
|
70
|
+
raise Util.CertoraUserInputError(f"Invalid file_to_mutate in manual mutant: {mutant[Constants.FILE_TO_MUTATE]}", e)
|
|
71
|
+
|
|
72
|
+
mutants_location = mutant[Constants.MUTANTS_LOCATION]
|
|
73
|
+
if Path(mutants_location).is_dir():
|
|
74
|
+
try:
|
|
75
|
+
Vf.validate_dir(mutants_location)
|
|
76
|
+
except Exception as e:
|
|
77
|
+
raise Util.CertoraUserInputError(f"Invalid directory for mutants location {mutants_location}",
|
|
78
|
+
e)
|
|
79
|
+
else:
|
|
80
|
+
try:
|
|
81
|
+
Vf.validate_readable_file(mutants_location, Util.SOL_EXT)
|
|
82
|
+
except Exception as e:
|
|
83
|
+
raise Util.CertoraUserInputError(f"Invalid file for mutants location {mutants_location}", e)
|
|
44
84
|
|
|
45
85
|
def mutation_attribute_in_prover(self) -> None:
|
|
46
86
|
gambit_attrs = ['filename', 'contract', 'functions', 'seed', 'num_mutants']
|
|
@@ -118,8 +158,8 @@ class MutateValidator:
|
|
|
118
158
|
|
|
119
159
|
if value is None:
|
|
120
160
|
raise RuntimeError(f"calling validate_type_string with null value {key}")
|
|
121
|
-
if not isinstance(value, str
|
|
122
|
-
raise Util.CertoraUserInputError(f"value of {key} {value} is not a string")
|
|
161
|
+
if not isinstance(value, (str, Path, int)):
|
|
162
|
+
raise Util.CertoraUserInputError(f"value of {key} {value} is not a string, integer, or Path")
|
|
123
163
|
attr.validate_value(str(value))
|
|
124
164
|
|
|
125
165
|
def validate_type_any(self, attr: AttrUtil.AttributeDefinition) -> None:
|
|
@@ -167,7 +167,12 @@ class Attributes:
|
|
|
167
167
|
table.add_column(Text("Description"), width=desc_col_width)
|
|
168
168
|
table.add_column(Text("Default"), width=default_col_width)
|
|
169
169
|
|
|
170
|
+
unsupported_attribute_names = [attr.name for attr in cls.unsupported_attributes()]
|
|
170
171
|
for name in dir(cls):
|
|
172
|
+
if name in cls.hide_attributes():
|
|
173
|
+
continue
|
|
174
|
+
if name in unsupported_attribute_names:
|
|
175
|
+
continue
|
|
171
176
|
if name.isupper():
|
|
172
177
|
attr = getattr(cls, name, None)
|
|
173
178
|
assert isinstance(attr, AttributeDefinition), "print_attr_help: type(attr) == Attribute"
|
|
@@ -188,8 +193,19 @@ class Attributes:
|
|
|
188
193
|
v.name = name
|
|
189
194
|
return v
|
|
190
195
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
+
cls._attribute_list = [set_name(name) for name in dir(cls) if name.isupper()]
|
|
197
|
+
cls._all_conf_names = [attr.name.lower() for attr in cls.attribute_list()]
|
|
198
|
+
cls._all_map_attrs = [attr for attr in cls._all_conf_names if attr.endswith(Util.MAP_SUFFIX)]
|
|
199
|
+
|
|
200
|
+
@classmethod
|
|
201
|
+
def hide_attributes(cls) -> List[str]:
|
|
202
|
+
"""
|
|
203
|
+
This function is used to hide attributes from the help message.
|
|
204
|
+
:return: A list of attribute names to be hidden.
|
|
205
|
+
"""
|
|
206
|
+
return []
|
|
207
|
+
|
|
208
|
+
@classmethod
|
|
209
|
+
def unsupported_attributes(cls) -> list:
|
|
210
|
+
# Return a list of AttributeDefinition objects that are unsupported
|
|
211
|
+
return []
|
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
15
15
|
|
|
16
16
|
import csv
|
|
17
|
-
import fnmatch
|
|
18
17
|
import json
|
|
19
18
|
import os
|
|
19
|
+
import io
|
|
20
|
+
import secrets
|
|
20
21
|
import subprocess
|
|
21
22
|
from abc import ABCMeta
|
|
22
23
|
from enum import Enum, unique, auto
|
|
@@ -31,15 +32,15 @@ import urllib3.util
|
|
|
31
32
|
from collections import defaultdict
|
|
32
33
|
from types import SimpleNamespace
|
|
33
34
|
|
|
34
|
-
from typing import Any, Callable, Dict, List, Optional, Set, Union, Generator, Tuple, Iterable, Sequence, TypeVar
|
|
35
|
+
from typing import Any, Callable, Dict, List, Optional, Set, Union, Generator, Tuple, Iterable, Sequence, TypeVar, OrderedDict
|
|
35
36
|
from pathlib import Path
|
|
37
|
+
import json5
|
|
36
38
|
|
|
37
39
|
scripts_dir_path = Path(__file__).parent.parent.resolve() # containing directory
|
|
38
40
|
sys.path.insert(0, str(scripts_dir_path))
|
|
39
41
|
from contextlib import contextmanager
|
|
40
42
|
from Shared.ExpectedComparator import ExpectedComparator
|
|
41
43
|
import logging
|
|
42
|
-
import random
|
|
43
44
|
import time
|
|
44
45
|
import tempfile
|
|
45
46
|
from datetime import datetime
|
|
@@ -91,13 +92,14 @@ CERTORA_RUN_SCRIPT = "certoraRun.py"
|
|
|
91
92
|
CERTORA_RUN_APP = "certoraRun"
|
|
92
93
|
PACKAGE_FILE = Path("package.json")
|
|
93
94
|
REMAPPINGS_FILE = Path("remappings.txt")
|
|
95
|
+
FOUNDRY_TOML_FILE = Path("foundry.toml")
|
|
94
96
|
RECENT_JOBS_FILE = Path(".certora_recent_jobs.json")
|
|
95
97
|
LAST_CONF_FILE = Path("run.conf")
|
|
96
98
|
EMV_JAR = Path("emv.jar")
|
|
97
99
|
CERTORA_SOURCES = Path(".certora_sources")
|
|
98
|
-
SOLANA_DEFAULT_COMMAND = "cargo +solana build-sbf"
|
|
99
100
|
SOLANA_INLINING = "solana_inlining"
|
|
100
101
|
SOLANA_SUMMARIES = "solana_summaries"
|
|
102
|
+
CARGO_TOML_FILE = "cargo.toml"
|
|
101
103
|
|
|
102
104
|
ALPHA_PACKAGE_NAME = 'certora-cli-alpha'
|
|
103
105
|
ALPHA_PACKAGE_MASTER_NAME = ALPHA_PACKAGE_NAME + '-master'
|
|
@@ -115,13 +117,17 @@ EVM_SOURCE_EXTENSIONS = (SOL_EXT, VY_EXT, YUL_EXT)
|
|
|
115
117
|
EVM_EXTENSIONS = EVM_SOURCE_EXTENSIONS + ('.tac', '.json')
|
|
116
118
|
SOLANA_EXEC_EXTENSION = '.so'
|
|
117
119
|
SOROBAN_EXEC_EXTENSION = '.wasm'
|
|
118
|
-
|
|
120
|
+
VALID_EVM_EXTENSIONS = list(EVM_EXTENSIONS) + ['.conf']
|
|
121
|
+
VALID_FILE_EXTENSIONS = VALID_EVM_EXTENSIONS + [SOLANA_EXEC_EXTENSION, SOROBAN_EXEC_EXTENSION]
|
|
119
122
|
# Type alias definition, not a variable
|
|
120
123
|
CompilerVersion = Tuple[int, int, int]
|
|
121
124
|
MAP_SUFFIX = '_map'
|
|
122
125
|
SUPPRESS_HELP_MSG = "==SUPPRESS=="
|
|
123
126
|
MAX_FLAG_LENGTH = 31
|
|
124
127
|
HELP_TABLE_WIDTH = 97
|
|
128
|
+
DEFAULT_RANGER_RANGE = '5'
|
|
129
|
+
DEFAULT_RANGER_LOOP_ITER = '3'
|
|
130
|
+
DEFAULT_RANGER_FAILURE_LIMIT = '1'
|
|
125
131
|
|
|
126
132
|
T = TypeVar('T')
|
|
127
133
|
|
|
@@ -151,7 +157,7 @@ def get_build_dir() -> Path:
|
|
|
151
157
|
|
|
152
158
|
def get_random_build_dir() -> Path:
|
|
153
159
|
for tries in range(3):
|
|
154
|
-
build_uuid = f"{datetime.now().strftime('%y_%m_%d_%H_%M_%S')}_{
|
|
160
|
+
build_uuid = f"{datetime.now().strftime('%y_%m_%d_%H_%M_%S')}_{secrets.randbelow(1_000_000):06d}{secrets.token_hex(2)}"
|
|
155
161
|
build_dir = CERTORA_INTERNAL_ROOT / Path(build_uuid)
|
|
156
162
|
if not build_dir.exists():
|
|
157
163
|
return build_dir
|
|
@@ -259,6 +265,8 @@ def get_debug_log_file() -> Path:
|
|
|
259
265
|
def get_extension_info_file() -> Path:
|
|
260
266
|
return path_in_build_directory(Path(".vscode_extension_info.json"))
|
|
261
267
|
|
|
268
|
+
def get_asts_file() -> Path:
|
|
269
|
+
return path_in_build_directory(Path(".asts.json"))
|
|
262
270
|
|
|
263
271
|
def get_zip_output_url_file() -> Path:
|
|
264
272
|
return CERTORA_INTERNAL_ROOT / '.zip-output-url.txt'
|
|
@@ -296,8 +304,12 @@ class ImplementationError(Exception):
|
|
|
296
304
|
class BadMutationError(Exception):
|
|
297
305
|
pass
|
|
298
306
|
|
|
307
|
+
class ExitException(Exception):
|
|
308
|
+
def __init__(self, message: str, exit_code: int):
|
|
309
|
+
super().__init__(message)
|
|
310
|
+
self.exit_code = exit_code # Store the integer data
|
|
299
311
|
|
|
300
|
-
MIN_JAVA_VERSION =
|
|
312
|
+
MIN_JAVA_VERSION = 19 # minimal java version to run the local type checker jar
|
|
301
313
|
|
|
302
314
|
|
|
303
315
|
def text_style(txt: str, style: str) -> str:
|
|
@@ -362,11 +374,8 @@ def remove_file(file_path: Union[str, Path]) -> None: # TODO - accept only Path
|
|
|
362
374
|
except OSError:
|
|
363
375
|
pass
|
|
364
376
|
else:
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
file_path.unlink()
|
|
368
|
-
except FileNotFoundError:
|
|
369
|
-
pass
|
|
377
|
+
file_path.unlink(missing_ok=True)
|
|
378
|
+
|
|
370
379
|
|
|
371
380
|
def abs_norm_path(file_path: Union[str, Path]) -> Path:
|
|
372
381
|
"""
|
|
@@ -507,6 +516,18 @@ def safe_copy_folder(source: Path, dest: Path, ignore_patterns: Callable[[str, L
|
|
|
507
516
|
shutil.rmtree(copy_temp, ignore_errors=True)
|
|
508
517
|
|
|
509
518
|
|
|
519
|
+
def safe_merge_folder(source: Path, dest: Path, ignore_patterns: Callable[[str, List[str]], Iterable[str]]) -> None:
|
|
520
|
+
"""
|
|
521
|
+
Safely merge source to dest. dest may exist. Will overwrite existing files
|
|
522
|
+
On certain OS/kernels/FS, copying a folder f into a subdirectory of f will
|
|
523
|
+
send copy tree into an infinite loop. This sidesteps the problem by first copying through a temporary folder.
|
|
524
|
+
"""
|
|
525
|
+
copy_temp = tempfile.mkdtemp()
|
|
526
|
+
shutil.copytree(source, copy_temp, ignore=ignore_patterns, dirs_exist_ok=True)
|
|
527
|
+
shutil.copytree(copy_temp, dest, dirs_exist_ok=True)
|
|
528
|
+
shutil.rmtree(copy_temp, ignore_errors=True)
|
|
529
|
+
|
|
530
|
+
|
|
510
531
|
def as_posix(path: str) -> str:
|
|
511
532
|
"""
|
|
512
533
|
Converts path from windows to unix
|
|
@@ -953,18 +974,6 @@ def flatten_set_list(set_list: List[Set[Any]]) -> List[Any]:
|
|
|
953
974
|
return list(ret_set)
|
|
954
975
|
|
|
955
976
|
|
|
956
|
-
def is_relative_to(path1: Path, path2: Path) -> bool:
|
|
957
|
-
"""certora-cli currently requires python3.8 and it's the last version without support for is_relative_to.
|
|
958
|
-
Shamelessly copying.
|
|
959
|
-
"""
|
|
960
|
-
# return path1.is_relative_to(path2)
|
|
961
|
-
try:
|
|
962
|
-
path1.relative_to(path2)
|
|
963
|
-
return True
|
|
964
|
-
except ValueError:
|
|
965
|
-
return False
|
|
966
|
-
|
|
967
|
-
|
|
968
977
|
def find_jar(jar_name: str) -> Path:
|
|
969
978
|
# if we are a dev running certoraRun.py (local version), we want to get the local jar
|
|
970
979
|
# if we are a dev running an installed version of certoraRun, we want to get the installed jar
|
|
@@ -976,7 +985,7 @@ def find_jar(jar_name: str) -> Path:
|
|
|
976
985
|
|
|
977
986
|
if certora_home != "":
|
|
978
987
|
local_certora_path = Path(certora_home) / CERTORA_JARS / jar_name
|
|
979
|
-
if
|
|
988
|
+
if Path(__file__).is_relative_to(Path(certora_home)) and local_certora_path.is_file():
|
|
980
989
|
return local_certora_path
|
|
981
990
|
|
|
982
991
|
return get_package_resource(CERTORA_JARS / jar_name)
|
|
@@ -991,31 +1000,46 @@ def get_package_resource(resource: Path) -> Path:
|
|
|
991
1000
|
return Path(__file__).parents[2] / resource
|
|
992
1001
|
|
|
993
1002
|
|
|
994
|
-
def
|
|
1003
|
+
def get_java_version() -> str:
|
|
995
1004
|
"""
|
|
996
|
-
|
|
997
|
-
@return
|
|
1005
|
+
Retrieve installed java version
|
|
1006
|
+
@return installed java version on success or empty string
|
|
998
1007
|
"""
|
|
999
1008
|
# Check if java exists on the machine
|
|
1000
1009
|
java = which("java")
|
|
1001
1010
|
if java is None:
|
|
1011
|
+
return ''
|
|
1012
|
+
|
|
1013
|
+
try:
|
|
1014
|
+
java_version_str = subprocess.check_output(['java', '-version'], stderr=subprocess.STDOUT).decode()
|
|
1015
|
+
java_version = re.search(r'version \"([\d\.]+)\"', java_version_str).groups()[0] # type: ignore[union-attr]
|
|
1016
|
+
|
|
1017
|
+
return java_version
|
|
1018
|
+
except (subprocess.CalledProcessError, AttributeError):
|
|
1019
|
+
typecheck_logger.debug("Couldn't find the installed Java version.")
|
|
1020
|
+
return ''
|
|
1021
|
+
|
|
1022
|
+
|
|
1023
|
+
def is_java_installed(java_version: str) -> bool:
|
|
1024
|
+
"""
|
|
1025
|
+
Check that java is installed and with a version that is suitable for running certora jars
|
|
1026
|
+
@return True on success
|
|
1027
|
+
"""
|
|
1028
|
+
if not java_version:
|
|
1002
1029
|
typecheck_logger.warning(
|
|
1003
1030
|
f"`java` is not installed. Installing Java version {MIN_JAVA_VERSION} or later will enable faster "
|
|
1004
1031
|
f"CVL specification syntax checking before uploading to the cloud.")
|
|
1005
1032
|
return False
|
|
1006
1033
|
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
major_java_version = re.search(r'version \"(\d+).*', java_version_str).groups()[0] # type: ignore[union-attr]
|
|
1034
|
+
else:
|
|
1035
|
+
major_java_version = java_version.split('.')[0]
|
|
1010
1036
|
if int(major_java_version) < MIN_JAVA_VERSION:
|
|
1011
1037
|
typecheck_logger.warning("Installed Java version is too old to check CVL specification files locally. "
|
|
1012
1038
|
f" Java version should be at least {MIN_JAVA_VERSION} to allow local java-based "
|
|
1013
1039
|
"type checking")
|
|
1014
1040
|
|
|
1015
1041
|
return False
|
|
1016
|
-
|
|
1017
|
-
typecheck_logger.warning("Couldn't find the installed Java version.")
|
|
1018
|
-
return False
|
|
1042
|
+
|
|
1019
1043
|
return True
|
|
1020
1044
|
|
|
1021
1045
|
|
|
@@ -1165,26 +1189,9 @@ class Singleton(type):
|
|
|
1165
1189
|
cls.__instancesinstances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
|
|
1166
1190
|
return cls.__instancesinstances[cls]
|
|
1167
1191
|
|
|
1168
|
-
|
|
1169
1192
|
class AbstractAndSingleton(Singleton, ABCMeta):
|
|
1170
1193
|
pass
|
|
1171
1194
|
|
|
1172
|
-
|
|
1173
|
-
def match_path_to_mapping_key(path: Path, m: Dict[str, str]) -> Optional[str]:
|
|
1174
|
-
"""
|
|
1175
|
-
Matches the path to the best match in the dictionary's keys.
|
|
1176
|
-
For example, given an absolute path `/Users/JohnDoe/Path/ToSolc/a.sol`, if the map contains
|
|
1177
|
-
`b/a.sol` and `ToSolc/a.sol`, it will match on `ToSolc/a.sol`.
|
|
1178
|
-
@param path: the path to match against
|
|
1179
|
-
@param m: the map whose keys we're searching
|
|
1180
|
-
@return: the value from the map that best matches the path, None if not found.
|
|
1181
|
-
"""
|
|
1182
|
-
for k, v in m.items():
|
|
1183
|
-
if fnmatch.fnmatch(str(path), k):
|
|
1184
|
-
return v
|
|
1185
|
-
return None
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
1195
|
def find_in(dir_path: Path, file_to_find: Path) -> Optional[Path]:
|
|
1189
1196
|
"""
|
|
1190
1197
|
Given a directory dir_path and a file we wish to find within that directory,
|
|
@@ -1256,7 +1263,7 @@ class TestValue(NoValEnum):
|
|
|
1256
1263
|
determines the chekpoint where the execution will halt. The exception TestResultsReady will be thrown. The value
|
|
1257
1264
|
will also determine what object will be attached to the exception for inspection by the caller
|
|
1258
1265
|
"""
|
|
1259
|
-
|
|
1266
|
+
BEFORE_LOCAL_PROVER_CALL = auto()
|
|
1260
1267
|
CHECK_ARGS = auto()
|
|
1261
1268
|
AFTER_BUILD = auto()
|
|
1262
1269
|
CHECK_SOLC_OPTIONS = auto()
|
|
@@ -1269,6 +1276,9 @@ class TestValue(NoValEnum):
|
|
|
1269
1276
|
AFTER_GENERATE_COLLECT_REPORT = auto()
|
|
1270
1277
|
AFTER_BUILD_RUST = auto()
|
|
1271
1278
|
AFTER_RULE_SPLIT = auto()
|
|
1279
|
+
SOLANA_BUILD_CMD = auto()
|
|
1280
|
+
CHECK_ZIP = auto()
|
|
1281
|
+
STORAGE_EXTENSION_LAYOUT = auto()
|
|
1272
1282
|
|
|
1273
1283
|
class FeValue(NoValEnum):
|
|
1274
1284
|
PRODUCTION = auto()
|
|
@@ -1357,25 +1367,80 @@ def is_local(object: Any) -> bool:
|
|
|
1357
1367
|
default_jar_path = Path(certora_root_dir) / EMV_JAR
|
|
1358
1368
|
return getattr(object, 'jar', None) or (default_jar_path.is_file() and not getattr(object, 'server', None))
|
|
1359
1369
|
|
|
1370
|
+
def get_mappings_from_forge_remappings() -> List[str]:
|
|
1371
|
+
remappings_output = ""
|
|
1372
|
+
try:
|
|
1373
|
+
result = subprocess.run(['forge', 'remappings'], capture_output=True, text=True, check=True)
|
|
1374
|
+
remappings_output = result.stdout
|
|
1375
|
+
except Exception as e:
|
|
1376
|
+
context_logger.debug(f"get_mappings_from_forge_remappings: forge failed to run\n{e}")
|
|
1377
|
+
|
|
1378
|
+
remappings = []
|
|
1379
|
+
if remappings_output:
|
|
1380
|
+
for line in remappings_output.strip().split('\n'):
|
|
1381
|
+
key, value = line.split('=', 1)
|
|
1382
|
+
if key and value:
|
|
1383
|
+
remappings.append(line.strip())
|
|
1384
|
+
for suffix in ['contracts/', 'src/']:
|
|
1385
|
+
if value.endswith(suffix) and not key.endswith(suffix):
|
|
1386
|
+
new_remapping = f"{key}{suffix}={value}"
|
|
1387
|
+
if new_remapping not in remappings:
|
|
1388
|
+
remappings.append(new_remapping)
|
|
1389
|
+
|
|
1390
|
+
return remappings
|
|
1391
|
+
|
|
1392
|
+
def check_remapping_file() -> None:
|
|
1393
|
+
seen: Dict[str, str] = {}
|
|
1394
|
+
|
|
1395
|
+
if not REMAPPINGS_FILE.exists():
|
|
1396
|
+
return
|
|
1397
|
+
with open(REMAPPINGS_FILE) as f:
|
|
1398
|
+
for lineno, line in enumerate(f, 1):
|
|
1399
|
+
line = line.strip()
|
|
1400
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
1401
|
+
continue
|
|
1402
|
+
parts = line.split("=")
|
|
1403
|
+
if len(parts) != 2:
|
|
1404
|
+
raise CertoraUserInputError(f"Invalid remapping in {REMAPPINGS_FILE} line {lineno}: {line}")
|
|
1405
|
+
key, value = map(str.strip, parts)
|
|
1406
|
+
|
|
1407
|
+
if key in seen:
|
|
1408
|
+
if seen[key] == value:
|
|
1409
|
+
io_logger.warning(f"Warning: duplicate key-value pair at line {lineno}: {key}={value}")
|
|
1410
|
+
else:
|
|
1411
|
+
raise CertoraUserInputError(f"Conflicting values in {REMAPPINGS_FILE} for key '{key}' "
|
|
1412
|
+
f"at line {lineno} (previous: '{seen[key]}', new: '{value}')")
|
|
1413
|
+
else:
|
|
1414
|
+
seen[key] = value
|
|
1415
|
+
|
|
1360
1416
|
|
|
1361
1417
|
def handle_remappings_file(context: SimpleNamespace) -> List[str]:
|
|
1362
1418
|
""""
|
|
1363
|
-
Tries to reach packages from remappings.txt
|
|
1419
|
+
Tries to reach packages from remappings.txt.
|
|
1420
|
+
If the file exists in cwd and foundry.toml does not exist in cwd we return the mappings in
|
|
1421
|
+
the file (legacy implementation).
|
|
1422
|
+
In all other cases we add the remappings returned from running the "forge remappings" command. Forge remappings
|
|
1423
|
+
takes into consideration mappings in remappings.txt but also mappings in foundry.toml and mappings from auto scan
|
|
1364
1424
|
:return:
|
|
1365
1425
|
"""
|
|
1366
|
-
|
|
1426
|
+
remappings = []
|
|
1427
|
+
check_remapping_file()
|
|
1428
|
+
if REMAPPINGS_FILE.exists() and not FOUNDRY_TOML_FILE.exists():
|
|
1367
1429
|
try:
|
|
1368
1430
|
with REMAPPINGS_FILE.open() as remappings_file:
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
if len(set(keys)) < len(keys):
|
|
1372
|
-
raise CertoraUserInputError(f"remappings.txt includes duplicated keys in: {keys}")
|
|
1373
|
-
return remappings
|
|
1431
|
+
remappings_set = set(filter(lambda x: x != "", map(lambda x: x.strip(), remappings_file.readlines())))
|
|
1432
|
+
remappings = list(remappings_set)
|
|
1374
1433
|
except CertoraUserInputError as e:
|
|
1375
1434
|
raise e from None
|
|
1376
1435
|
except Exception as e:
|
|
1436
|
+
# create CertoraUserInputError from other exceptions
|
|
1377
1437
|
raise CertoraUserInputError(f"Invalid remappings file: {REMAPPINGS_FILE}", e)
|
|
1378
|
-
|
|
1438
|
+
elif find_nearest_foundry_toml():
|
|
1439
|
+
remappings = get_mappings_from_forge_remappings()
|
|
1440
|
+
|
|
1441
|
+
context.forge_remappings = remappings
|
|
1442
|
+
|
|
1443
|
+
return remappings
|
|
1379
1444
|
|
|
1380
1445
|
|
|
1381
1446
|
def get_ir_flag(solc: str) -> str:
|
|
@@ -1443,3 +1508,58 @@ def eq_by(f: Callable[[T, T], bool], a: Sequence[T], b: Sequence[T]) -> bool:
|
|
|
1443
1508
|
check if Sequences a and b are equal according to function f.
|
|
1444
1509
|
"""
|
|
1445
1510
|
return len(a) == len(b) and all(map(f, a, b))
|
|
1511
|
+
|
|
1512
|
+
|
|
1513
|
+
def find_file_in_parents(file_name: Union[Path, str]) -> Optional[Path]:
|
|
1514
|
+
"""
|
|
1515
|
+
find file_name in current directory or in one of its parent directories
|
|
1516
|
+
"""
|
|
1517
|
+
current = Path.cwd()
|
|
1518
|
+
for parent in [current] + list(current.parents):
|
|
1519
|
+
candidate = parent / str(file_name)
|
|
1520
|
+
if candidate.is_file():
|
|
1521
|
+
return candidate
|
|
1522
|
+
return None
|
|
1523
|
+
|
|
1524
|
+
def find_nearest_cargo_toml() -> Optional[Path]:
|
|
1525
|
+
"""
|
|
1526
|
+
Find the nearest Cargo.toml file in the current directory or its parent directories.
|
|
1527
|
+
Returns the path to the Cargo.toml file if found, otherwise returns None.
|
|
1528
|
+
"""
|
|
1529
|
+
return find_file_in_parents(CARGO_TOML_FILE)
|
|
1530
|
+
|
|
1531
|
+
def find_nearest_foundry_toml() -> Optional[Path]:
|
|
1532
|
+
"""
|
|
1533
|
+
Find the nearest foundry.toml file in the current directory or its parent directories.
|
|
1534
|
+
Returns the path to the foundry.toml file if found, otherwise returns None.
|
|
1535
|
+
"""
|
|
1536
|
+
return find_file_in_parents(FOUNDRY_TOML_FILE)
|
|
1537
|
+
|
|
1538
|
+
|
|
1539
|
+
def file_in_source_tree(file_path: Path) -> bool:
|
|
1540
|
+
# if the file is under .certora_source, return True
|
|
1541
|
+
file_path = Path(file_path).absolute()
|
|
1542
|
+
parent_dir = get_certora_sources_dir().absolute()
|
|
1543
|
+
|
|
1544
|
+
try:
|
|
1545
|
+
file_path.relative_to(parent_dir)
|
|
1546
|
+
return True
|
|
1547
|
+
except ValueError:
|
|
1548
|
+
return False
|
|
1549
|
+
|
|
1550
|
+
def parse_int_as_str(val: str) -> str:
|
|
1551
|
+
return str(val)
|
|
1552
|
+
|
|
1553
|
+
def read_conf_file(file: io.TextIOWrapper) -> OrderedDict:
|
|
1554
|
+
res = json5.load(file, allow_duplicate_keys=False, object_pairs_hook=OrderedDict, parse_int=parse_int_as_str)
|
|
1555
|
+
return res
|
|
1556
|
+
|
|
1557
|
+
def convert_str_ints(obj: Union[dict, list, str, int]) -> Union[dict, list, str, int]:
|
|
1558
|
+
if isinstance(obj, dict):
|
|
1559
|
+
return {k: convert_str_ints(v) for k, v in obj.items()}
|
|
1560
|
+
elif isinstance(obj, list):
|
|
1561
|
+
return [convert_str_ints(v) for v in obj]
|
|
1562
|
+
elif isinstance(obj, str) and re.fullmatch(r"-?\d+", obj):
|
|
1563
|
+
return int(obj)
|
|
1564
|
+
else:
|
|
1565
|
+
return obj
|