lean-runtime 2.0.2__tar.gz → 2.0.3__tar.gz

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 (111) hide show
  1. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/CHANGELOG.md +7 -0
  2. {lean_runtime-2.0.2/lean_runtime.egg-info → lean_runtime-2.0.3}/PKG-INFO +1 -1
  3. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/bundles.py +54 -8
  4. {lean_runtime-2.0.2 → lean_runtime-2.0.3/lean_runtime.egg-info}/PKG-INFO +1 -1
  5. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/pyproject.toml +1 -1
  6. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/tests/test_bundles.py +53 -1
  7. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/LICENSE +0 -0
  8. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/MANIFEST.in +0 -0
  9. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/README.md +0 -0
  10. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/compatibility/README.md +0 -0
  11. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/compatibility/mathlib-4.32.2.json +0 -0
  12. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/compatibility/mathlib-4.32.2.toml +0 -0
  13. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/docs/architecture.md +0 -0
  14. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/docs/captures.md +0 -0
  15. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/docs/case-study-v1.md +0 -0
  16. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/docs/cli.md +0 -0
  17. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/docs/development.md +0 -0
  18. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/docs/environments.md +0 -0
  19. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/docs/getting-started.md +0 -0
  20. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/docs/index.md +0 -0
  21. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/docs/local-projects.md +0 -0
  22. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/docs/portable-copies.md +0 -0
  23. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/docs/python-api.md +0 -0
  24. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/docs/ready-programs.md +0 -0
  25. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/docs/standalone-files.md +0 -0
  26. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/docs/trust-and-limitations.md +0 -0
  27. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/docs/v1-precision.md +0 -0
  28. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/examples/mathlib.toml +0 -0
  29. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/__init__.py +0 -0
  30. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/__main__.py +0 -0
  31. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/_git.py +0 -0
  32. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/_paths.py +0 -0
  33. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/backends.py +0 -0
  34. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/cli.py +0 -0
  35. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/comparison.py +0 -0
  36. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/decisions.py +0 -0
  37. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/diagnostics.py +0 -0
  38. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/environments.py +0 -0
  39. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/errors.py +0 -0
  40. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/events.py +0 -0
  41. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/facade.py +0 -0
  42. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/frontmatter.py +0 -0
  43. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/health.py +0 -0
  44. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/lake.py +0 -0
  45. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/lockfiles.py +0 -0
  46. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/locking.py +0 -0
  47. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/matrix.py +0 -0
  48. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/models.py +0 -0
  49. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/oci.py +0 -0
  50. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/policies.py +0 -0
  51. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/profiling.py +0 -0
  52. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/programs.py +0 -0
  53. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/projects.py +0 -0
  54. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/publisher_verification.py +0 -0
  55. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/py.typed +0 -0
  56. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/references.py +0 -0
  57. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/resolver.py +0 -0
  58. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/run_cli.py +0 -0
  59. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/runtime.py +0 -0
  60. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/schema_resources.py +0 -0
  61. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/serialization.py +0 -0
  62. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/specs.py +0 -0
  63. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/store.py +0 -0
  64. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/timings.py +0 -0
  65. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/toolchains.py +0 -0
  66. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/verification.py +0 -0
  67. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime/wire.py +0 -0
  68. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime.egg-info/SOURCES.txt +0 -0
  69. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime.egg-info/dependency_links.txt +0 -0
  70. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime.egg-info/entry_points.txt +0 -0
  71. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime.egg-info/requires.txt +0 -0
  72. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/lean_runtime.egg-info/top_level.txt +0 -0
  73. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/mkdocs.yml +0 -0
  74. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/schemas/cleanup-v1.schema.json +0 -0
  75. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/schemas/comparison-v1.schema.json +0 -0
  76. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/schemas/execution-v1.schema.json +0 -0
  77. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/schemas/inspect-v1.schema.json +0 -0
  78. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/schemas/matrix-v1.schema.json +0 -0
  79. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/schemas/profile-v1.schema.json +0 -0
  80. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/schemas/verify-v1.schema.json +0 -0
  81. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/scripts/run_compatibility.py +0 -0
  82. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/scripts/run_v1_case_study.py +0 -0
  83. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/scripts/smoke_wheel.py +0 -0
  84. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/setup.cfg +0 -0
  85. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/tests/conftest.py +0 -0
  86. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/tests/test_backend.py +0 -0
  87. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/tests/test_cli.py +0 -0
  88. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/tests/test_diagnostics.py +0 -0
  89. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/tests/test_environment_integration.py +0 -0
  90. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/tests/test_events_health.py +0 -0
  91. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/tests/test_facade.py +0 -0
  92. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/tests/test_frontmatter.py +0 -0
  93. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/tests/test_git.py +0 -0
  94. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/tests/test_interactive.py +0 -0
  95. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/tests/test_lockfiles.py +0 -0
  96. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/tests/test_multifile.py +0 -0
  97. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/tests/test_paths.py +0 -0
  98. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/tests/test_prebuilt_policy.py +0 -0
  99. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/tests/test_programs.py +0 -0
  100. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/tests/test_projects.py +0 -0
  101. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/tests/test_references.py +0 -0
  102. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/tests/test_run_cli.py +0 -0
  103. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/tests/test_runtime.py +0 -0
  104. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/tests/test_schema_resources.py +0 -0
  105. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/tests/test_schemas.py +0 -0
  106. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/tests/test_signatures.py +0 -0
  107. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/tests/test_specs.py +0 -0
  108. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/tests/test_store.py +0 -0
  109. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/tests/test_toolchains.py +0 -0
  110. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/tests/test_v1_precision.py +0 -0
  111. {lean_runtime-2.0.2 → lean_runtime-2.0.3}/tests/test_verification_inventory.py +0 -0
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.0.3
4
+
5
+ - Accept portable-copy symlinks whose normalized targets remain inside the
6
+ extracted copy, including common links such as `docs/README.md -> ../README.md`.
7
+ - Continue rejecting absolute or escaping symlink targets, duplicate archive
8
+ members, and later archive writes that traverse an extracted symlink.
9
+
3
10
  ## 2.0.2
4
11
 
5
12
  - Remove staged environments safely when Git-for-Windows leaves source pack
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lean-runtime
3
- Version: 2.0.2
3
+ Version: 2.0.3
4
4
  Summary: Run Lean 4 proofs from Python or standalone files
5
5
  Author: Alejandro Radisic
6
6
  License-Expression: Apache-2.0
@@ -203,11 +203,54 @@ def _require_media_type(descriptor: dict[str, Any], expected: str, label: str) -
203
203
 
204
204
  def _safe_name(name: str) -> PurePosixPath:
205
205
  path = PurePosixPath(name)
206
- if not name or path.is_absolute() or ".." in path.parts or "\x00" in name:
206
+ if (
207
+ not name
208
+ or path == PurePosixPath(".")
209
+ or path.is_absolute()
210
+ or ".." in path.parts
211
+ or "\\" in name
212
+ or "\x00" in name
213
+ ):
207
214
  raise EnvironmentError(f"unsafe bundle member path: {name!r}")
208
215
  return path
209
216
 
210
217
 
218
+ def _internal_link_target(member: PurePosixPath, linkname: str) -> PurePosixPath:
219
+ link = PurePosixPath(linkname)
220
+ if not linkname or link.is_absolute() or "\\" in linkname or "\x00" in linkname:
221
+ raise EnvironmentError(f"unsafe bundle symlink: {member.as_posix()!r}")
222
+ parts: list[str] = []
223
+ for part in member.parent.joinpath(link).parts:
224
+ if part in {"", "."}:
225
+ continue
226
+ if part == "..":
227
+ if not parts:
228
+ raise EnvironmentError(f"unsafe bundle symlink: {member.as_posix()!r}")
229
+ parts.pop()
230
+ else:
231
+ parts.append(part)
232
+ return PurePosixPath(*parts) if parts else PurePosixPath(".")
233
+
234
+
235
+ def _ensure_directories(destination: Path, relative: PurePosixPath) -> None:
236
+ current = destination
237
+ for part in relative.parts:
238
+ if part == ".":
239
+ continue
240
+ current = current / part
241
+ if current.is_symlink():
242
+ raise EnvironmentError(
243
+ f"bundle member traverses an extracted symlink: {relative.as_posix()!r}"
244
+ )
245
+ if current.exists():
246
+ if not current.is_dir():
247
+ raise EnvironmentError(
248
+ f"bundle member parent is not a directory: {relative.as_posix()!r}"
249
+ )
250
+ else:
251
+ current.mkdir()
252
+
253
+
211
254
  def _packages_directory(lock: EnvironmentLock) -> PurePosixPath:
212
255
  value = lock.manifest.get("packagesDir", ".lake/packages")
213
256
  if not isinstance(value, str):
@@ -221,6 +264,8 @@ def _packages_directory(lock: EnvironmentLock) -> PurePosixPath:
221
264
  def _extract_layer(data: bytes | Path, destination: Path) -> None:
222
265
  total = 0
223
266
  count = 0
267
+ if destination.is_symlink():
268
+ raise EnvironmentError("bundle extraction destination must not be a symlink")
224
269
  destination.mkdir(parents=True, exist_ok=True)
225
270
  try:
226
271
  if isinstance(data, Path):
@@ -240,10 +285,12 @@ def _extract_layer(data: bytes | Path, destination: Path) -> None:
240
285
  relative = _safe_name(member.name)
241
286
  target = destination.joinpath(*relative.parts)
242
287
  if member.isdir():
243
- target.mkdir(parents=True, exist_ok=True)
288
+ _ensure_directories(destination, relative)
244
289
  target.chmod(member.mode & 0o777)
245
290
  elif member.isfile():
246
- target.parent.mkdir(parents=True, exist_ok=True)
291
+ _ensure_directories(destination, relative.parent)
292
+ if target.exists() or target.is_symlink():
293
+ raise EnvironmentError(f"duplicate bundle member: {member.name!r}")
247
294
  source = archive.extractfile(member)
248
295
  if source is None:
249
296
  raise EnvironmentError(f"bundle member has no content: {member.name}")
@@ -251,11 +298,10 @@ def _extract_layer(data: bytes | Path, destination: Path) -> None:
251
298
  shutil.copyfileobj(source, handle)
252
299
  target.chmod(member.mode & 0o777)
253
300
  elif member.issym():
254
- link = PurePosixPath(member.linkname)
255
- resolved = relative.parent.joinpath(link)
256
- if link.is_absolute() or ".." in resolved.parts:
257
- raise EnvironmentError(f"unsafe bundle symlink: {member.name!r}")
258
- target.parent.mkdir(parents=True, exist_ok=True)
301
+ _internal_link_target(relative, member.linkname)
302
+ _ensure_directories(destination, relative.parent)
303
+ if target.exists() or target.is_symlink():
304
+ raise EnvironmentError(f"duplicate bundle member: {member.name!r}")
259
305
  target.symlink_to(member.linkname)
260
306
  else:
261
307
  raise EnvironmentError(f"unsupported bundle member: {member.name!r}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lean-runtime
3
- Version: 2.0.2
3
+ Version: 2.0.3
4
4
  Summary: Run Lean 4 proofs from Python or standalone files
5
5
  Author: Alejandro Radisic
6
6
  License-Expression: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lean-runtime"
7
- version = "2.0.2"
7
+ version = "2.0.3"
8
8
  description = "Run Lean 4 proofs from Python or standalone files"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import hashlib
4
4
  import http.server
5
+ import io
5
6
  import json
6
7
  import os
7
8
  import shutil
@@ -15,7 +16,7 @@ from pathlib import Path
15
16
  import pytest
16
17
 
17
18
  from lean_runtime import EnvironmentError, EnvironmentLock, LockedPackage, Runtime
18
- from lean_runtime.bundles import _oci_archive
19
+ from lean_runtime.bundles import _extract_layer, _oci_archive
19
20
  from lean_runtime.environments import ENVIRONMENT_SCHEMA
20
21
  from lean_runtime.oci import OCIEnvironmentPublisher, OCIRepository
21
22
  from lean_runtime.store import environment_identity, platform_record, source_snapshot_digest
@@ -119,6 +120,57 @@ def _archive_entries(path: Path) -> dict[str, bytes]:
119
120
  return result
120
121
 
121
122
 
123
+ def _layer(*members: tuple[str, str, bytes | str]) -> bytes:
124
+ output = io.BytesIO()
125
+ with tarfile.open(fileobj=output, mode="w:gz", format=tarfile.PAX_FORMAT) as archive:
126
+ for kind, name, value in members:
127
+ info = tarfile.TarInfo(name)
128
+ info.mode = 0o644
129
+ if kind == "file":
130
+ assert isinstance(value, bytes)
131
+ info.size = len(value)
132
+ archive.addfile(info, io.BytesIO(value))
133
+ else:
134
+ assert kind == "symlink" and isinstance(value, str)
135
+ info.type = tarfile.SYMTYPE
136
+ info.linkname = value
137
+ archive.addfile(info)
138
+ return output.getvalue()
139
+
140
+
141
+ @pytest.mark.skipif(os.name == "nt", reason="Windows runners may not permit symlink creation")
142
+ def test_layer_extract_accepts_internal_parent_symlink(tmp_path: Path) -> None:
143
+ layer = _layer(
144
+ ("file", "README.md", b"Batteries\n"),
145
+ ("symlink", "docs/README.md", "../README.md"),
146
+ )
147
+
148
+ _extract_layer(layer, tmp_path / "output")
149
+
150
+ link = tmp_path / "output" / "docs" / "README.md"
151
+ assert link.is_symlink()
152
+ assert link.read_text() == "Batteries\n"
153
+
154
+
155
+ @pytest.mark.skipif(os.name == "nt", reason="Windows runners may not permit symlink creation")
156
+ def test_layer_extract_rejects_symlink_outside_destination(tmp_path: Path) -> None:
157
+ layer = _layer(("symlink", "docs/README.md", "../../outside"))
158
+
159
+ with pytest.raises(EnvironmentError, match="unsafe bundle symlink"):
160
+ _extract_layer(layer, tmp_path / "output")
161
+
162
+
163
+ @pytest.mark.skipif(os.name == "nt", reason="Windows runners may not permit symlink creation")
164
+ def test_layer_extract_rejects_later_write_through_symlink(tmp_path: Path) -> None:
165
+ layer = _layer(
166
+ ("symlink", "redirect", "inside"),
167
+ ("file", "redirect/payload", b"must not be written"),
168
+ )
169
+
170
+ with pytest.raises(EnvironmentError, match="traverses an extracted symlink"):
171
+ _extract_layer(layer, tmp_path / "output")
172
+
173
+
122
174
  def test_bundle_export_is_deterministic_and_imports_into_fresh_store(tmp_path: Path) -> None:
123
175
  producer, environment_id, lock = _published_runtime(tmp_path / "producer")
124
176
  first = tmp_path / "first.oci.tar.gz"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes