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.
Files changed (54) hide show
  1. certora_cli/CertoraProver/Compiler/CompilerCollectorFactory.py +10 -3
  2. certora_cli/CertoraProver/Compiler/CompilerCollectorVy.py +51 -16
  3. certora_cli/CertoraProver/Compiler/CompilerCollectorYul.py +3 -0
  4. certora_cli/CertoraProver/castingInstrumenter.py +192 -0
  5. certora_cli/CertoraProver/certoraApp.py +52 -0
  6. certora_cli/CertoraProver/certoraBuild.py +694 -207
  7. certora_cli/CertoraProver/certoraBuildCacheManager.py +21 -17
  8. certora_cli/CertoraProver/certoraBuildDataClasses.py +8 -2
  9. certora_cli/CertoraProver/certoraBuildRust.py +88 -54
  10. certora_cli/CertoraProver/certoraBuildSui.py +112 -0
  11. certora_cli/CertoraProver/certoraCloudIO.py +97 -96
  12. certora_cli/CertoraProver/certoraCollectConfigurationLayout.py +230 -84
  13. certora_cli/CertoraProver/certoraCollectRunMetadata.py +52 -6
  14. certora_cli/CertoraProver/certoraCompilerParameters.py +11 -0
  15. certora_cli/CertoraProver/certoraConfigIO.py +43 -35
  16. certora_cli/CertoraProver/certoraContext.py +128 -54
  17. certora_cli/CertoraProver/certoraContextAttributes.py +415 -234
  18. certora_cli/CertoraProver/certoraContextValidator.py +152 -105
  19. certora_cli/CertoraProver/certoraContractFuncs.py +34 -1
  20. certora_cli/CertoraProver/certoraParseBuildScript.py +8 -10
  21. certora_cli/CertoraProver/certoraType.py +10 -1
  22. certora_cli/CertoraProver/certoraVerifyGenerator.py +22 -4
  23. certora_cli/CertoraProver/erc7201.py +45 -0
  24. certora_cli/CertoraProver/splitRules.py +23 -18
  25. certora_cli/CertoraProver/storageExtension.py +351 -0
  26. certora_cli/EquivalenceCheck/Eq_default.conf +0 -1
  27. certora_cli/EquivalenceCheck/Eq_sanity.conf +0 -1
  28. certora_cli/EquivalenceCheck/equivCheck.py +2 -1
  29. certora_cli/Mutate/mutateApp.py +41 -22
  30. certora_cli/Mutate/mutateAttributes.py +11 -0
  31. certora_cli/Mutate/mutateValidate.py +42 -2
  32. certora_cli/Shared/certoraAttrUtil.py +21 -5
  33. certora_cli/Shared/certoraUtils.py +180 -60
  34. certora_cli/Shared/certoraValidateFuncs.py +68 -26
  35. certora_cli/Shared/proverCommon.py +308 -0
  36. certora_cli/certoraCVLFormatter.py +76 -0
  37. certora_cli/certoraConcord.py +39 -0
  38. certora_cli/certoraEVMProver.py +4 -3
  39. certora_cli/certoraRanger.py +39 -0
  40. certora_cli/certoraRun.py +83 -223
  41. certora_cli/certoraSolanaProver.py +40 -128
  42. certora_cli/certoraSorobanProver.py +59 -4
  43. certora_cli/certoraSuiProver.py +93 -0
  44. certora_cli_beta_mirror-8.5.0.dist-info/LICENSE +15 -0
  45. {certora_cli_beta_mirror-7.28.0.dist-info → certora_cli_beta_mirror-8.5.0.dist-info}/METADATA +21 -5
  46. certora_cli_beta_mirror-8.5.0.dist-info/RECORD +81 -0
  47. {certora_cli_beta_mirror-7.28.0.dist-info → certora_cli_beta_mirror-8.5.0.dist-info}/WHEEL +1 -1
  48. {certora_cli_beta_mirror-7.28.0.dist-info → certora_cli_beta_mirror-8.5.0.dist-info}/entry_points.txt +3 -0
  49. certora_jars/ASTExtraction.jar +0 -0
  50. certora_jars/CERTORA-CLI-VERSION-METADATA.json +1 -1
  51. certora_jars/Typechecker.jar +0 -0
  52. certora_cli_beta_mirror-7.28.0.dist-info/LICENSE +0 -22
  53. certora_cli_beta_mirror-7.28.0.dist-info/RECORD +0 -70
  54. {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) and not isinstance(value, Path):
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
- if not cls._attribute_list:
192
- cls._attribute_list = [set_name(name) for name in dir(cls) if name.isupper()]
193
- cls._all_conf_names = [attr.name.lower() for attr in cls.attribute_list()]
194
- # 'compiler_map' does not have a matching 'compiler' attribute
195
- cls._all_map_attrs = [attr for attr in cls._all_conf_names if attr.endswith(Util.MAP_SUFFIX)]
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
- VALID_FILE_EXTENSIONS = ['.conf'] + list(EVM_EXTENSIONS) + [SOLANA_EXEC_EXTENSION, SOROBAN_EXEC_EXTENSION]
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')}_{random.randint(0, 999):03d}"
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 = 11 # minimal java version to run the local type checker jar
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
- try:
366
- # When we upgrade to Python 3.8, we can use unlink(missing_ok=True) and remove the try/except clauses
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 is_relative_to(Path(__file__), Path(certora_home)) and local_certora_path.is_file():
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 is_java_installed() -> bool:
1003
+ def get_java_version() -> str:
995
1004
  """
996
- Check that java is installed and with a version that is suitable for running certora jars
997
- @return True on success
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
- try:
1008
- java_version_str = subprocess.check_output(['java', '-version'], stderr=subprocess.STDOUT).decode()
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
- except (subprocess.CalledProcessError, AttributeError):
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
- LOCAL_JAR = auto()
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
- if REMAPPINGS_FILE.exists():
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
- remappings = list(filter(lambda x: x != "", map(lambda x: x.strip(), remappings_file.readlines())))
1370
- keys = [s.split('=')[0] for s in remappings]
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
- return []
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