fluidattacks_core_filesystem 15.0.0__tar.gz → 16.0.0__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.
- fluidattacks_core_filesystem-16.0.0/.envrc +1 -0
- fluidattacks_core_filesystem-16.0.0/.gitignore +16 -0
- {fluidattacks_core_filesystem-15.0.0 → fluidattacks_core_filesystem-16.0.0}/PKG-INFO +1 -1
- fluidattacks_core_filesystem-16.0.0/fluidattacks_core/filesystem/cargo.py +110 -0
- {fluidattacks_core_filesystem-15.0.0 → fluidattacks_core_filesystem-16.0.0}/fluidattacks_core/filesystem/defaults.py +6 -0
- {fluidattacks_core_filesystem-15.0.0 → fluidattacks_core_filesystem-16.0.0}/fluidattacks_core/filesystem/partitioning.py +94 -9
- {fluidattacks_core_filesystem-15.0.0 → fluidattacks_core_filesystem-16.0.0}/import-linter.cfg +1 -1
- {fluidattacks_core_filesystem-15.0.0 → fluidattacks_core_filesystem-16.0.0}/pyproject.toml +1 -1
- {fluidattacks_core_filesystem-15.0.0 → fluidattacks_core_filesystem-16.0.0}/ruff.toml +3 -0
- fluidattacks_core_filesystem-16.0.0/tests/test_cargo.py +122 -0
- {fluidattacks_core_filesystem-15.0.0 → fluidattacks_core_filesystem-16.0.0}/tests/test_partitioning.py +140 -3
- {fluidattacks_core_filesystem-15.0.0 → fluidattacks_core_filesystem-16.0.0}/tests/test_projects.py +9 -0
- {fluidattacks_core_filesystem-15.0.0 → fluidattacks_core_filesystem-16.0.0}/uv.lock +1 -1
- fluidattacks_core_filesystem-15.0.0/.envrc +0 -1
- fluidattacks_core_filesystem-15.0.0/.gitignore +0 -9
- {fluidattacks_core_filesystem-15.0.0 → fluidattacks_core_filesystem-16.0.0}/fluidattacks_core/filesystem/__init__.py +0 -0
- {fluidattacks_core_filesystem-15.0.0 → fluidattacks_core_filesystem-16.0.0}/fluidattacks_core/filesystem/detection.py +0 -0
- {fluidattacks_core_filesystem-15.0.0 → fluidattacks_core_filesystem-16.0.0}/fluidattacks_core/filesystem/language.py +0 -0
- {fluidattacks_core_filesystem-15.0.0 → fluidattacks_core_filesystem-16.0.0}/fluidattacks_core/filesystem/parts.py +0 -0
- {fluidattacks_core_filesystem-15.0.0 → fluidattacks_core_filesystem-16.0.0}/fluidattacks_core/filesystem/projects.py +0 -0
- {fluidattacks_core_filesystem-15.0.0 → fluidattacks_core_filesystem-16.0.0}/fluidattacks_core/filesystem/py.typed +0 -0
- {fluidattacks_core_filesystem-15.0.0 → fluidattacks_core_filesystem-16.0.0}/mypy.ini +0 -0
- {fluidattacks_core_filesystem-15.0.0 → fluidattacks_core_filesystem-16.0.0}/tests/__init__.py +0 -0
- {fluidattacks_core_filesystem-15.0.0 → fluidattacks_core_filesystem-16.0.0}/tests/data/partitioning_snapshot.golden.json +0 -0
- {fluidattacks_core_filesystem-15.0.0 → fluidattacks_core_filesystem-16.0.0}/tests/test_defaults.py +0 -0
- {fluidattacks_core_filesystem-15.0.0 → fluidattacks_core_filesystem-16.0.0}/tests/test_extract_db.py +0 -0
- {fluidattacks_core_filesystem-15.0.0 → fluidattacks_core_filesystem-16.0.0}/tests/test_language_detection.py +0 -0
- {fluidattacks_core_filesystem-15.0.0 → fluidattacks_core_filesystem-16.0.0}/tests/test_partitioning_snapshot.py +0 -0
- {fluidattacks_core_filesystem-15.0.0 → fluidattacks_core_filesystem-16.0.0}/tests/test_parts.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
use flake ../../..#fluidattacks-core-filesystem
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
dist/
|
|
2
|
+
dist-*/
|
|
3
|
+
*.zip
|
|
4
|
+
/utils/terraform-modules/**/.terraform.lock.hcl
|
|
5
|
+
/.cargo-cache
|
|
6
|
+
target/
|
|
7
|
+
.direnv/
|
|
8
|
+
.venv/
|
|
9
|
+
.vscode/settings.json
|
|
10
|
+
.zed/settings.json
|
|
11
|
+
# each package's shell writes its own, beside the package rather than here
|
|
12
|
+
**/.vscode/settings.json
|
|
13
|
+
*.so
|
|
14
|
+
# the publish check builds into its own tree per architecture
|
|
15
|
+
target-*/
|
|
16
|
+
/develops-ci-analytics-output
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import tomllib
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from fnmatch import fnmatchcase
|
|
6
|
+
from pathlib import PurePosixPath
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from .defaults import CARGO_MAX_PATTERNS
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from collections.abc import Mapping
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"CARGO_MANIFEST",
|
|
17
|
+
"CargoWorkspace",
|
|
18
|
+
"is_cargo_manifest",
|
|
19
|
+
"is_workspace_member",
|
|
20
|
+
"parse_cargo_workspace",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
CARGO_MANIFEST = "Cargo.toml"
|
|
24
|
+
|
|
25
|
+
_FOLDED_MANIFEST = CARGO_MANIFEST.casefold()
|
|
26
|
+
|
|
27
|
+
_Components = tuple[str, ...]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class CargoWorkspace:
|
|
32
|
+
members: tuple[_Components, ...]
|
|
33
|
+
exclude: tuple[_Components, ...]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def is_cargo_manifest(name: str) -> bool:
|
|
37
|
+
return name.casefold() == _FOLDED_MANIFEST
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _components(path: str) -> _Components | None:
|
|
41
|
+
pure = PurePosixPath(path)
|
|
42
|
+
if pure.is_absolute() or ".." in pure.parts:
|
|
43
|
+
return None
|
|
44
|
+
return pure.parts
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _patterns(value: object) -> tuple[_Components, ...] | None:
|
|
48
|
+
if not isinstance(value, list) or len(value) > CARGO_MAX_PATTERNS:
|
|
49
|
+
return None
|
|
50
|
+
items = tuple(item for item in value if isinstance(item, str))
|
|
51
|
+
if len(items) != len(value):
|
|
52
|
+
return None
|
|
53
|
+
parsed = (_components(item) for item in items)
|
|
54
|
+
return tuple(parts for parts in parsed if parts is not None)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def parse_cargo_workspace(text: str) -> CargoWorkspace | None:
|
|
58
|
+
try:
|
|
59
|
+
document = tomllib.loads(text)
|
|
60
|
+
except (ValueError, RecursionError):
|
|
61
|
+
return None
|
|
62
|
+
table = document.get("workspace")
|
|
63
|
+
if not isinstance(table, dict):
|
|
64
|
+
return None
|
|
65
|
+
members = _patterns(table.get("members", []))
|
|
66
|
+
exclude = _patterns(table.get("exclude", []))
|
|
67
|
+
if members is None or exclude is None:
|
|
68
|
+
return None
|
|
69
|
+
return CargoWorkspace(members=members, exclude=exclude)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _glob_matches(pattern: _Components, path: _Components) -> bool:
|
|
73
|
+
reachable = frozenset({0})
|
|
74
|
+
for head in pattern:
|
|
75
|
+
if not reachable:
|
|
76
|
+
return False
|
|
77
|
+
if head == "**":
|
|
78
|
+
reachable = frozenset(range(min(reachable), len(path) + 1))
|
|
79
|
+
else:
|
|
80
|
+
reachable = frozenset(
|
|
81
|
+
index + 1
|
|
82
|
+
for index in reachable
|
|
83
|
+
if index < len(path) and fnmatchcase(path[index], head)
|
|
84
|
+
)
|
|
85
|
+
return len(path) in reachable
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _excludes(workspace: CargoWorkspace, relative: _Components) -> bool:
|
|
89
|
+
manifest = (*relative, CARGO_MANIFEST)
|
|
90
|
+
return any(manifest[: len(entry)] == entry for entry in workspace.exclude) and not any(
|
|
91
|
+
manifest[: len(member)] == member for member in workspace.members
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def is_workspace_member(
|
|
96
|
+
directory: Path,
|
|
97
|
+
workspaces: Mapping[Path, CargoWorkspace],
|
|
98
|
+
root: Path,
|
|
99
|
+
) -> bool:
|
|
100
|
+
if directory in workspaces or directory == root:
|
|
101
|
+
return False
|
|
102
|
+
for ancestor in directory.parents:
|
|
103
|
+
workspace = workspaces.get(ancestor)
|
|
104
|
+
if workspace is not None:
|
|
105
|
+
relative = directory.relative_to(ancestor).parts
|
|
106
|
+
if not _excludes(workspace, relative):
|
|
107
|
+
return any(_glob_matches(member, relative) for member in workspace.members)
|
|
108
|
+
if ancestor == root:
|
|
109
|
+
return False
|
|
110
|
+
return False
|
|
@@ -3,11 +3,13 @@ from typing import TypedDict
|
|
|
3
3
|
from .language import Language
|
|
4
4
|
|
|
5
5
|
__all__ = [
|
|
6
|
+
"CARGO_MAX_PATTERNS",
|
|
6
7
|
"CONFIG_MARKERS",
|
|
7
8
|
"EXCLUDE_DIRS",
|
|
8
9
|
"EXCLUDE_FILES",
|
|
9
10
|
"EXTENDED_EXCLUDE_DIRS",
|
|
10
11
|
"MANIFEST_MARKERS",
|
|
12
|
+
"MANIFEST_MAX_BYTES",
|
|
11
13
|
"SKIP_DIRS",
|
|
12
14
|
"TEST_DATA_DIRS",
|
|
13
15
|
"TEST_FILES",
|
|
@@ -31,6 +33,10 @@ class WorkspaceSpec(TypedDict):
|
|
|
31
33
|
|
|
32
34
|
BUNDLE_DIR_GLOBS = ["*.xcodeproj", "*.xcworkspace"]
|
|
33
35
|
|
|
36
|
+
MANIFEST_MAX_BYTES = 50 * 1024
|
|
37
|
+
|
|
38
|
+
CARGO_MAX_PATTERNS = 1000
|
|
39
|
+
|
|
34
40
|
|
|
35
41
|
WORKSPACE_MARKERS: dict[str, WorkspaceSpec] = {
|
|
36
42
|
"gradle": {
|
|
@@ -10,7 +10,18 @@ from fnmatch import fnmatch, fnmatchcase
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from typing import TYPE_CHECKING
|
|
12
12
|
|
|
13
|
-
from .
|
|
13
|
+
from .cargo import (
|
|
14
|
+
CargoWorkspace,
|
|
15
|
+
is_cargo_manifest,
|
|
16
|
+
is_workspace_member,
|
|
17
|
+
parse_cargo_workspace,
|
|
18
|
+
)
|
|
19
|
+
from .defaults import (
|
|
20
|
+
BUNDLE_DIR_GLOBS,
|
|
21
|
+
EXTENDED_EXCLUDE_DIRS,
|
|
22
|
+
MANIFEST_MAX_BYTES,
|
|
23
|
+
TEST_SCAFFOLD_GLOBS,
|
|
24
|
+
)
|
|
14
25
|
from .detection import (
|
|
15
26
|
declared_workspaces,
|
|
16
27
|
detect_anchor_language,
|
|
@@ -91,6 +102,45 @@ def _claiming_kinds(dir_path: Path, workspaces: Mapping[Path, frozenset[str]]) -
|
|
|
91
102
|
return frozenset(kinds)
|
|
92
103
|
|
|
93
104
|
|
|
105
|
+
def _cargo_workspaces(cargo_manifests: Mapping[Path, str]) -> Mapping[Path, CargoWorkspace]:
|
|
106
|
+
return {
|
|
107
|
+
dir_path: workspace
|
|
108
|
+
for dir_path, text in cargo_manifests.items()
|
|
109
|
+
if (workspace := parse_cargo_workspace(text)) is not None
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _foreign_part_between(
|
|
114
|
+
dir_path: Path,
|
|
115
|
+
workspaces: Mapping[Path, CargoWorkspace],
|
|
116
|
+
names_by_dir: Mapping[Path, tuple[str, ...]],
|
|
117
|
+
) -> bool:
|
|
118
|
+
for ancestor in dir_path.parents:
|
|
119
|
+
if ancestor in workspaces:
|
|
120
|
+
return False
|
|
121
|
+
names = [name for name in names_by_dir.get(ancestor, ()) if not is_cargo_manifest(name)]
|
|
122
|
+
if detect_anchor_language(names) is not None:
|
|
123
|
+
return True
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _cargo_claimed_dirs(
|
|
128
|
+
listing: DirListing,
|
|
129
|
+
cargo_manifests: Mapping[Path, str],
|
|
130
|
+
root_path: Path,
|
|
131
|
+
) -> frozenset[Path]:
|
|
132
|
+
workspaces = _cargo_workspaces(cargo_manifests)
|
|
133
|
+
names_by_dir = dict(listing)
|
|
134
|
+
return frozenset(
|
|
135
|
+
dir_path
|
|
136
|
+
for dir_path, filenames in listing
|
|
137
|
+
if workspaces
|
|
138
|
+
and any(is_cargo_manifest(name) for name in filenames)
|
|
139
|
+
and is_workspace_member(dir_path, workspaces, root_path)
|
|
140
|
+
and not _foreign_part_between(dir_path, workspaces, names_by_dir)
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
94
144
|
def _is_bundle_name(name: str) -> bool:
|
|
95
145
|
return any(fnmatch(name, pattern) for pattern in BUNDLE_DIR_GLOBS)
|
|
96
146
|
|
|
@@ -123,24 +173,33 @@ def _anchor_names(
|
|
|
123
173
|
dir_path: Path,
|
|
124
174
|
filenames: Sequence[str],
|
|
125
175
|
workspaces: Mapping[Path, frozenset[str]],
|
|
176
|
+
cargo_claimed: frozenset[Path],
|
|
126
177
|
root_path: Path,
|
|
127
178
|
) -> tuple[str, ...]:
|
|
128
179
|
if _within_bundle(dir_path, root_path):
|
|
129
180
|
return ()
|
|
130
181
|
kinds = _claiming_kinds(dir_path, workspaces)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
182
|
+
claims_cargo = dir_path in cargo_claimed
|
|
183
|
+
return tuple(
|
|
184
|
+
name
|
|
185
|
+
for name in filenames
|
|
186
|
+
if not is_claimed_manifest(name, kinds) and not (claims_cargo and is_cargo_manifest(name))
|
|
187
|
+
)
|
|
134
188
|
|
|
135
189
|
|
|
136
|
-
def _discover_potential_parts(
|
|
190
|
+
def _discover_potential_parts(
|
|
191
|
+
listing: DirListing,
|
|
192
|
+
root_path: Path,
|
|
193
|
+
cargo_manifests: Mapping[Path, str],
|
|
194
|
+
) -> tuple[PotentialPart, ...]:
|
|
137
195
|
workspaces = _workspace_kinds_by_dir(listing)
|
|
196
|
+
cargo_claimed = _cargo_claimed_dirs(listing, cargo_manifests, root_path)
|
|
138
197
|
return tuple(
|
|
139
198
|
(dir_path, _find_config_files(dir_path, filenames), language.value)
|
|
140
199
|
for dir_path, filenames in listing
|
|
141
200
|
if (
|
|
142
201
|
language := detect_anchor_language(
|
|
143
|
-
_anchor_names(dir_path, filenames, workspaces, root_path)
|
|
202
|
+
_anchor_names(dir_path, filenames, workspaces, cargo_claimed, root_path)
|
|
144
203
|
)
|
|
145
204
|
)
|
|
146
205
|
is not None
|
|
@@ -262,8 +321,13 @@ def _build_single_root_tree(root_path: Path, listing: DirListing) -> PartTree:
|
|
|
262
321
|
)
|
|
263
322
|
|
|
264
323
|
|
|
265
|
-
def decide_part_tree(
|
|
266
|
-
|
|
324
|
+
def decide_part_tree(
|
|
325
|
+
root_path: Path,
|
|
326
|
+
listing: DirListing,
|
|
327
|
+
*,
|
|
328
|
+
cargo_manifests: Mapping[Path, str] | None = None,
|
|
329
|
+
) -> PartTree:
|
|
330
|
+
potential_parts = _discover_potential_parts(listing, root_path, cargo_manifests or {})
|
|
267
331
|
if not potential_parts:
|
|
268
332
|
return _build_single_root_tree(root_path, listing)
|
|
269
333
|
|
|
@@ -315,6 +379,26 @@ def _walk_listing(root_path: Path, exclude_dirs: Collection[str]) -> DirListing:
|
|
|
315
379
|
return tuple(listing)
|
|
316
380
|
|
|
317
381
|
|
|
382
|
+
def _read_manifest(path: Path, root_path: Path) -> str | None:
|
|
383
|
+
try:
|
|
384
|
+
if not path.resolve().is_relative_to(root_path):
|
|
385
|
+
return None
|
|
386
|
+
with os.fdopen(os.open(path, os.O_RDONLY | os.O_NOFOLLOW), "rb") as handle:
|
|
387
|
+
data = handle.read(MANIFEST_MAX_BYTES + 1)
|
|
388
|
+
return None if len(data) > MANIFEST_MAX_BYTES else data.decode("utf-8")
|
|
389
|
+
except (OSError, UnicodeDecodeError):
|
|
390
|
+
return None
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _read_cargo_manifests(listing: DirListing, root_path: Path) -> Mapping[Path, str]:
|
|
394
|
+
return {
|
|
395
|
+
dir_path: text
|
|
396
|
+
for dir_path, filenames in listing
|
|
397
|
+
for name in filenames
|
|
398
|
+
if is_cargo_manifest(name) and (text := _read_manifest(dir_path / name, root_path))
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
|
|
318
402
|
def build_part_tree(root_path: Path, extra_excludes: Sequence[str] | None = None) -> PartTree:
|
|
319
403
|
resolved = Path(root_path).resolve()
|
|
320
404
|
if not resolved.is_dir():
|
|
@@ -327,4 +411,5 @@ def build_part_tree(root_path: Path, extra_excludes: Sequence[str] | None = None
|
|
|
327
411
|
msg = f"Root path could not be read: {root_path}"
|
|
328
412
|
raise OSError(msg)
|
|
329
413
|
|
|
330
|
-
|
|
414
|
+
manifests = _read_cargo_manifests(listing, resolved)
|
|
415
|
+
return decide_part_tree(resolved, listing, cargo_manifests=manifests)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from fluidattacks_core.filesystem.cargo import (
|
|
7
|
+
CargoWorkspace,
|
|
8
|
+
is_cargo_manifest,
|
|
9
|
+
is_workspace_member,
|
|
10
|
+
parse_cargo_workspace,
|
|
11
|
+
)
|
|
12
|
+
from fluidattacks_core.filesystem.defaults import CARGO_MAX_PATTERNS
|
|
13
|
+
|
|
14
|
+
_ROOT = Path("/component")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _workspace(members: list[str], exclude: list[str] | None = None) -> CargoWorkspace:
|
|
18
|
+
text = f"[workspace]\nmembers = {json.dumps(members)}\nexclude = {json.dumps(exclude or [])}"
|
|
19
|
+
workspace = parse_cargo_workspace(text)
|
|
20
|
+
assert workspace is not None
|
|
21
|
+
return workspace
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _is_member(path: str, members: list[str], exclude: list[str] | None = None) -> bool:
|
|
25
|
+
return is_workspace_member(_ROOT / path, {_ROOT: _workspace(members, exclude)}, _ROOT)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_a_workspace_table_parses_into_path_components_once() -> None:
|
|
29
|
+
text = '[workspace]\nmembers = ["./crates/*/", "../x", "/abs"]\nexclude = ["crates/old"]'
|
|
30
|
+
|
|
31
|
+
assert parse_cargo_workspace(text) == CargoWorkspace(
|
|
32
|
+
members=(("crates", "*"),), exclude=(("crates", "old"),)
|
|
33
|
+
)
|
|
34
|
+
assert parse_cargo_workspace("[workspace]") == CargoWorkspace(members=(), exclude=())
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.mark.parametrize(
|
|
38
|
+
"text",
|
|
39
|
+
[
|
|
40
|
+
"x = " + "[" * 5000 + "]" * 5000,
|
|
41
|
+
"x = " + "9" * 5000,
|
|
42
|
+
"[workspace",
|
|
43
|
+
"workspace = 1",
|
|
44
|
+
'[workspace]\nmembers = "crates/*"',
|
|
45
|
+
"[workspace]\nmembers = [1]",
|
|
46
|
+
f"[workspace]\nmembers = {json.dumps(['a'] * (CARGO_MAX_PATTERNS + 1))}",
|
|
47
|
+
f"[workspace]\nexclude = {json.dumps(['a'] * (CARGO_MAX_PATTERNS + 1))}",
|
|
48
|
+
],
|
|
49
|
+
)
|
|
50
|
+
def test_a_hostile_or_malformed_manifest_is_no_workspace(text: str) -> None:
|
|
51
|
+
assert parse_cargo_workspace(text) is None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_a_workspace_at_the_pattern_cap_still_parses() -> None:
|
|
55
|
+
workspace = _workspace(["a"] * CARGO_MAX_PATTERNS)
|
|
56
|
+
|
|
57
|
+
assert len(workspace.members) == CARGO_MAX_PATTERNS
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_the_manifest_name_folds_case() -> None:
|
|
61
|
+
assert is_cargo_manifest("cargo.TOML")
|
|
62
|
+
assert not is_cargo_manifest("Cargo.lock")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.mark.parametrize(
|
|
66
|
+
("pattern", "path", "expected"),
|
|
67
|
+
[
|
|
68
|
+
("crates/*", "crates/domain", True),
|
|
69
|
+
("crates/*", "crates/domain/sub", False),
|
|
70
|
+
("**/inner", "inner", True),
|
|
71
|
+
("**/inner", "a/b/inner", True),
|
|
72
|
+
("crate-?", "crate-a", True),
|
|
73
|
+
("crate-?", "crate-ab", False),
|
|
74
|
+
("crate-[ab]", "crate-b", True),
|
|
75
|
+
("crate-[!ab]", "crate-b", False),
|
|
76
|
+
("Crates/*", "crates/domain", False),
|
|
77
|
+
],
|
|
78
|
+
)
|
|
79
|
+
def test_a_member_pattern_matches_whole_components(
|
|
80
|
+
pattern: str, path: str, *, expected: bool
|
|
81
|
+
) -> None:
|
|
82
|
+
assert _is_member(path, [pattern]) is expected
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_many_recursive_wildcards_match_in_linear_time() -> None:
|
|
86
|
+
assert _is_member("a/x", ["**/" * 3000 + "x"])
|
|
87
|
+
assert not _is_member("/".join(["d"] * 20), ["/".join(["**"] * 12) + "/zz"])
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_an_exclusion_is_a_literal_path_prefix() -> None:
|
|
91
|
+
assert not _is_member("crates/old", ["crates/*"], ["crates/old"])
|
|
92
|
+
assert not _is_member("crates/old", ["crates/*"], ["crates/old/Cargo.toml"])
|
|
93
|
+
assert _is_member("crates/old", ["crates/*"], ["crates/*"])
|
|
94
|
+
assert _is_member("crates/old", ["crates/*"], ["crates/ol"])
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_only_a_literal_member_overrides_an_exclusion() -> None:
|
|
98
|
+
assert not _is_member("crates/old", ["crates/*"], ["crates"])
|
|
99
|
+
assert _is_member("crates/old", ["crates/*", "crates/old"], ["crates"])
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_an_excluded_crate_is_claimed_by_an_outer_workspace() -> None:
|
|
103
|
+
workspaces = {
|
|
104
|
+
_ROOT: _workspace(["tools/gen"]),
|
|
105
|
+
_ROOT / "tools": _workspace(["*"], ["gen"]),
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
assert is_workspace_member(_ROOT / "tools" / "gen", workspaces, _ROOT)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_a_workspace_root_and_the_root_are_never_members() -> None:
|
|
112
|
+
workspaces = {_ROOT: _workspace(["**"]), _ROOT / "nested": _workspace(["inner"])}
|
|
113
|
+
|
|
114
|
+
assert not is_workspace_member(_ROOT / "nested", workspaces, _ROOT)
|
|
115
|
+
assert not is_workspace_member(_ROOT, workspaces, _ROOT)
|
|
116
|
+
assert is_workspace_member(_ROOT / "nested" / "inner", workspaces, _ROOT)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_no_workspace_above_the_root_is_consulted() -> None:
|
|
120
|
+
workspaces = {Path("/"): _workspace(["**"])}
|
|
121
|
+
|
|
122
|
+
assert not is_workspace_member(_ROOT / "lib", workspaces, _ROOT)
|
|
@@ -4,7 +4,11 @@ from pathlib import Path
|
|
|
4
4
|
import pytest
|
|
5
5
|
|
|
6
6
|
from fluidattacks_core.filesystem import build_part_tree
|
|
7
|
-
from fluidattacks_core.filesystem.defaults import
|
|
7
|
+
from fluidattacks_core.filesystem.defaults import (
|
|
8
|
+
MANIFEST_MARKERS,
|
|
9
|
+
MANIFEST_MAX_BYTES,
|
|
10
|
+
TEST_SCAFFOLD_GLOBS,
|
|
11
|
+
)
|
|
8
12
|
from fluidattacks_core.filesystem.partitioning import (
|
|
9
13
|
DirListing,
|
|
10
14
|
PotentialPart,
|
|
@@ -17,6 +21,7 @@ from fluidattacks_core.filesystem.partitioning import (
|
|
|
17
21
|
_is_scaffold_name,
|
|
18
22
|
_normalized_dir_name,
|
|
19
23
|
_owning_part,
|
|
24
|
+
_read_manifest,
|
|
20
25
|
decide_part_tree,
|
|
21
26
|
)
|
|
22
27
|
|
|
@@ -117,7 +122,7 @@ def test_config_files_are_found_by_name_and_glob() -> None:
|
|
|
117
122
|
|
|
118
123
|
|
|
119
124
|
def test_only_directories_holding_a_manifest_become_parts() -> None:
|
|
120
|
-
discovered = _discover_potential_parts(_LISTING, _ROOT)
|
|
125
|
+
discovered = _discover_potential_parts(_LISTING, _ROOT, {})
|
|
121
126
|
assert [(path, language) for path, _, language in discovered] == [
|
|
122
127
|
(_ROOT, "Python"),
|
|
123
128
|
(_ROOT / "web", "JavaScript"),
|
|
@@ -125,7 +130,8 @@ def test_only_directories_holding_a_manifest_become_parts() -> None:
|
|
|
125
130
|
|
|
126
131
|
|
|
127
132
|
def test_a_lone_hint_does_not_make_a_potential_part() -> None:
|
|
128
|
-
|
|
133
|
+
listing = ((_ROOT / "tools", ("mypy.ini", "tox.ini")),)
|
|
134
|
+
assert _discover_potential_parts(listing, _ROOT, {}) == ()
|
|
129
135
|
|
|
130
136
|
|
|
131
137
|
def test_hierarchy_nests_parts_under_their_nearest_ancestor() -> None:
|
|
@@ -352,6 +358,137 @@ def test_a_gradle_module_without_a_settings_file_anchors_alone() -> None:
|
|
|
352
358
|
assert sorted(tree.parts) == [".", "deployment/acceptance-test"]
|
|
353
359
|
|
|
354
360
|
|
|
361
|
+
_CARGO_ROOT = Path("/component")
|
|
362
|
+
|
|
363
|
+
_CARGO_LISTING = (
|
|
364
|
+
(_CARGO_ROOT, ("Cargo.toml", "Cargo.lock")),
|
|
365
|
+
(_CARGO_ROOT / "crates" / "domain", ("Cargo.toml",)),
|
|
366
|
+
(_CARGO_ROOT / "crates" / "domain" / "src", ("lib.rs",)),
|
|
367
|
+
(_CARGO_ROOT / "crates" / "shell", ("Cargo.toml",)),
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _cargo_parts(listing: DirListing, manifests: dict[Path, str]) -> list[str]:
|
|
372
|
+
return sorted(decide_part_tree(_CARGO_ROOT, listing, cargo_manifests=manifests).parts)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def test_a_cargo_workspace_folds_its_members_into_one_part() -> None:
|
|
376
|
+
manifests = {_CARGO_ROOT: '[workspace]\nmembers = ["crates/*"]'}
|
|
377
|
+
|
|
378
|
+
tree = decide_part_tree(_CARGO_ROOT, _CARGO_LISTING, cargo_manifests=manifests)
|
|
379
|
+
|
|
380
|
+
assert sorted(tree.parts) == ["."]
|
|
381
|
+
root = tree.root
|
|
382
|
+
assert root is not None
|
|
383
|
+
assert root.detected_language == "rust"
|
|
384
|
+
assert Path("crates/domain/src/lib.rs") in root.files
|
|
385
|
+
assert Path("crates/shell/Cargo.toml") in root.files
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def test_a_root_package_keeps_a_crate_it_does_not_list() -> None:
|
|
389
|
+
manifest = '[package]\nname = "app"\n[workspace]\nmembers = ["crates/domain"]'
|
|
390
|
+
|
|
391
|
+
assert _cargo_parts(_CARGO_LISTING, {_CARGO_ROOT: manifest}) == [".", "crates/shell"]
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
@pytest.mark.parametrize(
|
|
395
|
+
"manifest",
|
|
396
|
+
[
|
|
397
|
+
'[package]\nname = "app"',
|
|
398
|
+
"[workspace",
|
|
399
|
+
"workspace = 1",
|
|
400
|
+
'[workspace]\nmembers = "crates/*"',
|
|
401
|
+
"[workspace]\nmembers = [1]",
|
|
402
|
+
'[workspace]\nmembers = ["crates/*"]\nexclude = "crates/shell"',
|
|
403
|
+
],
|
|
404
|
+
)
|
|
405
|
+
def test_a_root_without_a_usable_workspace_changes_nothing(manifest: str) -> None:
|
|
406
|
+
expected = [".", "crates/domain", "crates/shell"]
|
|
407
|
+
|
|
408
|
+
assert _cargo_parts(_CARGO_LISTING, {_CARGO_ROOT: manifest}) == expected
|
|
409
|
+
assert sorted(decide_part_tree(_CARGO_ROOT, _CARGO_LISTING).parts) == expected
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def test_a_nested_cargo_workspace_anchors_and_claims_its_own_members() -> None:
|
|
413
|
+
listing = (
|
|
414
|
+
(_CARGO_ROOT, ("Cargo.toml",)),
|
|
415
|
+
(_CARGO_ROOT / "crates" / "nested", ("Cargo.toml",)),
|
|
416
|
+
(_CARGO_ROOT / "crates" / "nested" / "inner", ("Cargo.toml",)),
|
|
417
|
+
)
|
|
418
|
+
manifests = {
|
|
419
|
+
_CARGO_ROOT: '[workspace]\nmembers = ["crates/*"]',
|
|
420
|
+
_CARGO_ROOT / "crates" / "nested": '[workspace]\nmembers = ["inner"]',
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
assert _cargo_parts(listing, manifests) == [".", "crates/nested"]
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def test_a_member_under_a_foreign_part_keeps_its_own_part() -> None:
|
|
427
|
+
desktop = _CARGO_ROOT / "apps" / "desktop"
|
|
428
|
+
listing = (
|
|
429
|
+
(_CARGO_ROOT, ("Cargo.toml",)),
|
|
430
|
+
(desktop, ("package.json",)),
|
|
431
|
+
(desktop / "src-tauri", ("Cargo.toml",)),
|
|
432
|
+
)
|
|
433
|
+
manifest = '[workspace]\nmembers = ["apps/desktop/src-tauri"]'
|
|
434
|
+
expected = [".", "apps/desktop", "apps/desktop/src-tauri"]
|
|
435
|
+
|
|
436
|
+
assert _cargo_parts(listing, {_CARGO_ROOT: manifest}) == expected
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def test_a_cargo_workspace_does_not_claim_a_foreign_manifest() -> None:
|
|
440
|
+
listing = (
|
|
441
|
+
(_CARGO_ROOT, ("Cargo.toml",)),
|
|
442
|
+
(_CARGO_ROOT / "crates" / "web", ("Cargo.toml", "package.json")),
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
tree = decide_part_tree(
|
|
446
|
+
_CARGO_ROOT, listing, cargo_manifests={_CARGO_ROOT: '[workspace]\nmembers = ["crates/*"]'}
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
web = tree.get_part("crates/web")
|
|
450
|
+
assert web is not None
|
|
451
|
+
assert web.detected_language == "JavaScript"
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
_WORKSPACE = b'[workspace]\nmembers = ["crates/*"]'
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _write_workspace(root: Path, manifest: bytes) -> None:
|
|
458
|
+
(root / "crates" / "lib").mkdir(parents=True)
|
|
459
|
+
(root / "crates" / "lib" / "Cargo.toml").write_text('[package]\nname = "lib"')
|
|
460
|
+
(root / "Cargo.toml").write_bytes(manifest)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def test_build_part_tree_reads_a_cargo_workspace_from_disk(tmp_path: Path) -> None:
|
|
464
|
+
_write_workspace(tmp_path, _WORKSPACE)
|
|
465
|
+
|
|
466
|
+
assert sorted(build_part_tree(tmp_path).parts) == ["."]
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
@pytest.mark.parametrize(
|
|
470
|
+
"manifest",
|
|
471
|
+
[_WORKSPACE + b"\n#" + b"x" * MANIFEST_MAX_BYTES, b"\xff" + _WORKSPACE],
|
|
472
|
+
)
|
|
473
|
+
def test_an_oversized_or_undecodable_cargo_root_folds_nothing(
|
|
474
|
+
tmp_path: Path, manifest: bytes
|
|
475
|
+
) -> None:
|
|
476
|
+
_write_workspace(tmp_path, manifest)
|
|
477
|
+
|
|
478
|
+
assert sorted(build_part_tree(tmp_path).parts) == [".", "crates/lib"]
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
@pytest.mark.parametrize("inside", [True, False])
|
|
482
|
+
def test_a_symlinked_manifest_is_never_read(tmp_path: Path, *, inside: bool) -> None:
|
|
483
|
+
root = tmp_path / "repo"
|
|
484
|
+
root.mkdir()
|
|
485
|
+
target = (root if inside else tmp_path) / "real.toml"
|
|
486
|
+
target.write_bytes(_WORKSPACE)
|
|
487
|
+
(root / "Cargo.toml").symlink_to(target)
|
|
488
|
+
|
|
489
|
+
assert _read_manifest(root / "Cargo.toml", root.resolve()) is None
|
|
490
|
+
|
|
491
|
+
|
|
355
492
|
def test_an_xcode_bundle_does_not_anchor_beside_its_project() -> None:
|
|
356
493
|
listing = (
|
|
357
494
|
(_ROOT, ("pubspec.yaml",)),
|
{fluidattacks_core_filesystem-15.0.0 → fluidattacks_core_filesystem-16.0.0}/tests/test_projects.py
RENAMED
|
@@ -273,6 +273,15 @@ def test_no_marker_name_is_excluded_as_an_artifact() -> None:
|
|
|
273
273
|
assert excluded == []
|
|
274
274
|
|
|
275
275
|
|
|
276
|
+
def test_find_projects_still_splits_a_cargo_workspace(tmp_path: Path) -> None:
|
|
277
|
+
(tmp_path / "lib").mkdir()
|
|
278
|
+
(tmp_path / "lib" / "Cargo.toml").write_text('[package]\nname = "lib"')
|
|
279
|
+
(tmp_path / "Cargo.toml").write_text('[workspace]\nmembers = ["lib"]')
|
|
280
|
+
|
|
281
|
+
assert _by_path(find_projects(tmp_path), tmp_path.resolve()) == {".": "rust", "lib": "rust"}
|
|
282
|
+
assert sorted(build_part_tree(tmp_path).parts) == ["."]
|
|
283
|
+
|
|
284
|
+
|
|
276
285
|
def test_find_projects_still_splits_a_gradle_workspace(tmp_path: Path) -> None:
|
|
277
286
|
_make(tmp_path, ["settings.gradle", "build.gradle", "mod/build.gradle"])
|
|
278
287
|
root = tmp_path.resolve()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
use_flake ".#python311.devShell"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fluidattacks_core_filesystem-15.0.0 → fluidattacks_core_filesystem-16.0.0}/tests/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{fluidattacks_core_filesystem-15.0.0 → fluidattacks_core_filesystem-16.0.0}/tests/test_defaults.py
RENAMED
|
File without changes
|
{fluidattacks_core_filesystem-15.0.0 → fluidattacks_core_filesystem-16.0.0}/tests/test_extract_db.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fluidattacks_core_filesystem-15.0.0 → fluidattacks_core_filesystem-16.0.0}/tests/test_parts.py
RENAMED
|
File without changes
|