jprotect 0.1.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.
- jprotect/__init__.py +4 -0
- jprotect/__main__.py +5 -0
- jprotect/cli/__init__.py +1 -0
- jprotect/cli/main.py +64 -0
- jprotect/config/__init__.py +1 -0
- jprotect/config/loader.py +55 -0
- jprotect/config/schema.py +29 -0
- jprotect/core/__init__.py +1 -0
- jprotect/core/compile/__init__.py +1 -0
- jprotect/core/compile/cython_backend.py +106 -0
- jprotect/core/discovery.py +27 -0
- jprotect/core/filtering.py +37 -0
- jprotect/core/pipeline.py +83 -0
- jprotect/core/postprocess.py +18 -0
- jprotect/core/report.py +61 -0
- jprotect/core/safety.py +32 -0
- jprotect/core/transform/__init__.py +1 -0
- jprotect/core/transform/ast_transformer.py +501 -0
- jprotect/core/transform/transformer.py +100 -0
- jprotect/errors.py +10 -0
- jprotect/utils/__init__.py +1 -0
- jprotect/utils/paths.py +9 -0
- jprotect-0.1.0.dist-info/METADATA +362 -0
- jprotect-0.1.0.dist-info/RECORD +28 -0
- jprotect-0.1.0.dist-info/WHEEL +5 -0
- jprotect-0.1.0.dist-info/entry_points.txt +2 -0
- jprotect-0.1.0.dist-info/licenses/LICENSE +118 -0
- jprotect-0.1.0.dist-info/top_level.txt +1 -0
jprotect/__init__.py
ADDED
jprotect/__main__.py
ADDED
jprotect/cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Package CLI cho jProtect."""
|
jprotect/cli/main.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
from jprotect.config.loader import load_from_cli, validate_config
|
|
4
|
+
from jprotect.core.pipeline import run_pipeline
|
|
5
|
+
from jprotect.core.report import print_summary
|
|
6
|
+
from jprotect.errors import JProtectError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
10
|
+
"""Tạo parser cho CLI và đăng ký các tham số của lệnh protect-module.
|
|
11
|
+
|
|
12
|
+
Ví dụ: phân tích ``protect-module --input a --output b --compile``.
|
|
13
|
+
"""
|
|
14
|
+
parser = argparse.ArgumentParser(description="jProtect CLI")
|
|
15
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
16
|
+
|
|
17
|
+
protect_parser = subparsers.add_parser("protect-module", help="Bảo vệ một module Odoo")
|
|
18
|
+
protect_parser.add_argument("--input", required=True, help="Đường dẫn thư mục module Odoo")
|
|
19
|
+
protect_parser.add_argument("--output", required=True, help="Đường dẫn output cho module đã bảo vệ")
|
|
20
|
+
protect_parser.add_argument("--mode", choices=["dist", "inplace"], default="dist")
|
|
21
|
+
protect_parser.add_argument("--dry-run", action="store_true", help="Chỉ quét và báo cáo, không ghi file")
|
|
22
|
+
protect_parser.add_argument(
|
|
23
|
+
"--compile",
|
|
24
|
+
action="store_true",
|
|
25
|
+
help="Compile các file đã tách thành nhị phân (.so/.pyd) bằng Cython",
|
|
26
|
+
)
|
|
27
|
+
protect_parser.add_argument(
|
|
28
|
+
"--include",
|
|
29
|
+
action="append",
|
|
30
|
+
help="Glob include, có thể lặp lại. Mặc định: models/**/*.py",
|
|
31
|
+
)
|
|
32
|
+
protect_parser.add_argument(
|
|
33
|
+
"--exclude",
|
|
34
|
+
action="append",
|
|
35
|
+
help="Glob exclude, có thể lặp lại.",
|
|
36
|
+
)
|
|
37
|
+
return parser
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def main() -> int:
|
|
41
|
+
"""Điểm vào CLI: đọc tham số, chạy pipeline và in kết quả.
|
|
42
|
+
|
|
43
|
+
Ví dụ: trả về 0 khi thành công, 2 khi gặp JProtectError.
|
|
44
|
+
"""
|
|
45
|
+
parser = build_parser()
|
|
46
|
+
args = parser.parse_args()
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
config = load_from_cli(args)
|
|
50
|
+
validate_config(config)
|
|
51
|
+
report = run_pipeline(config)
|
|
52
|
+
print_summary(report)
|
|
53
|
+
if config.dry_run:
|
|
54
|
+
print("Dry-run mode: no files were written.")
|
|
55
|
+
else:
|
|
56
|
+
print(f"Manifest : {report.get('manifest_path')}")
|
|
57
|
+
return 0
|
|
58
|
+
except JProtectError as exc:
|
|
59
|
+
print(f"Error: {exc}")
|
|
60
|
+
return 2
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Package cấu hình cho jProtect."""
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from jprotect.config.schema import DEFAULT_EXCLUDE_PATTERNS, DEFAULT_INCLUDE_PATTERNS, ProtectConfig
|
|
4
|
+
from jprotect.errors import ConfigError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _normalize_patterns(patterns: list[str] | None, defaults: list[str]) -> list[str]:
|
|
8
|
+
"""Chuẩn hóa danh sách pattern; trả về giá trị mặc định nếu rỗng.
|
|
9
|
+
|
|
10
|
+
Ví dụ: None -> defaults; [' ', ''] -> defaults; [' models/*.py '] -> ['models/*.py'].
|
|
11
|
+
"""
|
|
12
|
+
if not patterns:
|
|
13
|
+
return list(defaults)
|
|
14
|
+
normalized = [p.strip() for p in patterns if p and p.strip()]
|
|
15
|
+
if not normalized:
|
|
16
|
+
return list(defaults)
|
|
17
|
+
return normalized
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def load_from_cli(args) -> ProtectConfig:
|
|
21
|
+
"""Dựng đối tượng args từ argparse thành ProtectConfig.
|
|
22
|
+
|
|
23
|
+
Ví dụ: args(input='a', output='b', mode='dist', compile=True) ->
|
|
24
|
+
ProtectConfig tương ứng với đường dẫn đã resolve.
|
|
25
|
+
"""
|
|
26
|
+
include_patterns = _normalize_patterns(args.include, DEFAULT_INCLUDE_PATTERNS)
|
|
27
|
+
exclude_patterns = _normalize_patterns(args.exclude, DEFAULT_EXCLUDE_PATTERNS)
|
|
28
|
+
return ProtectConfig(
|
|
29
|
+
input_path=Path(args.input).resolve(),
|
|
30
|
+
output_path=Path(args.output).resolve(),
|
|
31
|
+
mode=args.mode,
|
|
32
|
+
dry_run=bool(args.dry_run),
|
|
33
|
+
compile=bool(getattr(args, "compile", False)),
|
|
34
|
+
include_patterns=include_patterns,
|
|
35
|
+
exclude_patterns=exclude_patterns,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def validate_config(config: ProtectConfig) -> None:
|
|
40
|
+
"""Kiểm tra tính hợp lệ của config trước khi chạy pipeline.
|
|
41
|
+
|
|
42
|
+
Ví dụ: mode lạ -> ConfigError; input không tồn tại -> ConfigError;
|
|
43
|
+
dist nhưng input trùng output -> ConfigError.
|
|
44
|
+
"""
|
|
45
|
+
if config.mode not in {"dist", "inplace"}:
|
|
46
|
+
raise ConfigError(f"Unsupported mode: {config.mode}")
|
|
47
|
+
|
|
48
|
+
if not config.input_path.exists() or not config.input_path.is_dir():
|
|
49
|
+
raise ConfigError(f"Input path does not exist or is not a directory: {config.input_path}")
|
|
50
|
+
|
|
51
|
+
if config.mode == "dist" and config.input_path == config.output_path:
|
|
52
|
+
raise ConfigError("Output path must differ from input path when mode is 'dist'.")
|
|
53
|
+
|
|
54
|
+
if config.mode == "inplace" and config.output_path != config.input_path:
|
|
55
|
+
raise ConfigError("Output path must equal input path when mode is 'inplace'.")
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
DEFAULT_INCLUDE_PATTERNS = ["models/**/*.py"]
|
|
6
|
+
DEFAULT_EXCLUDE_PATTERNS = [
|
|
7
|
+
"**/__init__.py",
|
|
8
|
+
"**/__manifest__.py",
|
|
9
|
+
"**/migrations/**",
|
|
10
|
+
"**/controllers/**",
|
|
11
|
+
"**/*hook*.py",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class ProtectConfig:
|
|
17
|
+
"""Cấu hình đầy đủ cho một lần chạy protect-module.
|
|
18
|
+
|
|
19
|
+
Ví dụ: ProtectConfig(input_path=Path('addons/sale_ext'),
|
|
20
|
+
output_path=Path('dist/sale_ext'), mode='dist', compile=True).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
input_path: Path
|
|
24
|
+
output_path: Path
|
|
25
|
+
mode: str = "dist"
|
|
26
|
+
dry_run: bool = False
|
|
27
|
+
compile: bool = False
|
|
28
|
+
include_patterns: list[str] = field(default_factory=lambda: list(DEFAULT_INCLUDE_PATTERNS))
|
|
29
|
+
exclude_patterns: list[str] = field(default_factory=lambda: list(DEFAULT_EXCLUDE_PATTERNS))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Package core (lõi xử lý) cho jProtect."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Các backend compile cho jProtect."""
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Backend compile Cython (Phase 3).
|
|
2
|
+
|
|
3
|
+
Nhận các file trong package _protected, build thành extension nhị phân (.so/.pyd),
|
|
4
|
+
sau đó xóa file .py và .c trung gian để chỉ giữ lại artifact đã bảo vệ.
|
|
5
|
+
|
|
6
|
+
Lưu ý quan trọng về Odoo: tên extension được đặt theo dạng
|
|
7
|
+
``<ten_addon>.<duong_dan_tuong_doi>`` và build từ thư mục cha của module. Nhờ vậy
|
|
8
|
+
``__module__`` của class compile bắt đầu bằng tên addon, giúp Odoo gán model đúng
|
|
9
|
+
addon (Odoo lấy thành phần đầu của ``__module__`` để xác định addon).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import shutil
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from jprotect.errors import JProtectError
|
|
17
|
+
|
|
18
|
+
ARTIFACT_SUFFIXES = {".so", ".pyd"}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _ensure_toolchain() -> None:
|
|
22
|
+
"""Kiểm tra Cython/setuptools đã sẵn sàng; nếu thiếu thì báo lỗi rõ ràng."""
|
|
23
|
+
try:
|
|
24
|
+
import Cython.Build # noqa: F401
|
|
25
|
+
import setuptools # noqa: F401
|
|
26
|
+
except ImportError as exc: # pragma: no cover - phụ thuộc môi trường
|
|
27
|
+
raise JProtectError(
|
|
28
|
+
"Thieu Cython/setuptools de compile. Cai bang: pip install \".[compile]\""
|
|
29
|
+
) from exc
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _module_name(file: Path, base_dir: Path) -> str:
|
|
33
|
+
"""Tạo tên module dạng chuỗi có dấu chấm từ đường dẫn tương đối (bỏ .py).
|
|
34
|
+
|
|
35
|
+
Ví dụ: file ``sale_ext/models/_protected/sale_order.py`` với base_dir là cha
|
|
36
|
+
của ``sale_ext`` -> 'sale_ext.models._protected.sale_order'.
|
|
37
|
+
"""
|
|
38
|
+
rel = file.relative_to(base_dir).with_suffix("")
|
|
39
|
+
return ".".join(rel.parts)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _collect_artifacts(protected_files: list[Path]) -> list[Path]:
|
|
43
|
+
"""Lấy danh sách artifact .so/.pyd bên cạnh từng file đã compile.
|
|
44
|
+
|
|
45
|
+
Ví dụ: cạnh ``sale_order.py`` tìm được ``sale_order.cp312-win_amd64.pyd``.
|
|
46
|
+
"""
|
|
47
|
+
artifacts: list[Path] = []
|
|
48
|
+
for file in protected_files:
|
|
49
|
+
for candidate in file.parent.glob(file.stem + ".*"):
|
|
50
|
+
if candidate.suffix in ARTIFACT_SUFFIXES:
|
|
51
|
+
artifacts.append(candidate)
|
|
52
|
+
return artifacts
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _cleanup_intermediates(protected_files: list[Path], base_dir: Path) -> None:
|
|
56
|
+
"""Xóa file .py và .c trung gian, và thư mục build tạm sau khi compile.
|
|
57
|
+
|
|
58
|
+
Ví dụ: sau khi build ``sale_order.py`` -> xóa ``sale_order.py``, ``sale_order.c``
|
|
59
|
+
và thư mục ``build/``, chỉ giữ lại file ``.pyd``/``.so``.
|
|
60
|
+
"""
|
|
61
|
+
for file in protected_files:
|
|
62
|
+
c_file = file.with_suffix(".c")
|
|
63
|
+
if c_file.exists():
|
|
64
|
+
c_file.unlink()
|
|
65
|
+
if file.exists():
|
|
66
|
+
file.unlink()
|
|
67
|
+
build_temp = base_dir / "build"
|
|
68
|
+
if build_temp.is_dir():
|
|
69
|
+
shutil.rmtree(build_temp, ignore_errors=True)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def compile_protected_files(files: list[Path], output_root: Path) -> list[Path]:
|
|
73
|
+
"""Build các file đã tách thành extension nhị phân (.so/.pyd) bằng Cython.
|
|
74
|
+
|
|
75
|
+
Ví dụ: với ``[.../_protected/sale_order.py]`` và output_root là thư mục module,
|
|
76
|
+
trả về ``[.../_protected/sale_order.cp312-win_amd64.pyd]``.
|
|
77
|
+
"""
|
|
78
|
+
targets = [f for f in files if f.name != "__init__.py"]
|
|
79
|
+
if not targets:
|
|
80
|
+
return []
|
|
81
|
+
_ensure_toolchain()
|
|
82
|
+
|
|
83
|
+
from Cython.Build import cythonize
|
|
84
|
+
from setuptools import Extension, setup
|
|
85
|
+
|
|
86
|
+
# Build từ thư mục cha của module để tên extension có tiền tố là tên addon.
|
|
87
|
+
base_dir = output_root.parent
|
|
88
|
+
|
|
89
|
+
cwd = Path.cwd()
|
|
90
|
+
os.chdir(base_dir)
|
|
91
|
+
try:
|
|
92
|
+
extensions = [
|
|
93
|
+
Extension(_module_name(f, base_dir), [f.relative_to(base_dir).as_posix()])
|
|
94
|
+
for f in targets
|
|
95
|
+
]
|
|
96
|
+
setup(
|
|
97
|
+
name="jprotect_build",
|
|
98
|
+
ext_modules=cythonize(extensions, language_level=3, quiet=True),
|
|
99
|
+
script_args=["build_ext", "--inplace"],
|
|
100
|
+
)
|
|
101
|
+
finally:
|
|
102
|
+
os.chdir(cwd)
|
|
103
|
+
|
|
104
|
+
artifacts = _collect_artifacts(targets)
|
|
105
|
+
_cleanup_intermediates(targets, base_dir)
|
|
106
|
+
return artifacts
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from jprotect.errors import ConfigError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def discover_module_root(input_path: Path) -> Path:
|
|
7
|
+
"""Xác minh thư mục đầu vào là module Odoo (có __manifest__.py).
|
|
8
|
+
|
|
9
|
+
Ví dụ: input ``/addons/sale_ext`` có ``__manifest__.py`` -> trả về chính nó;
|
|
10
|
+
nếu thiếu manifest -> ném ConfigError.
|
|
11
|
+
"""
|
|
12
|
+
manifest = input_path / "__manifest__.py"
|
|
13
|
+
if not manifest.exists():
|
|
14
|
+
raise ConfigError(f"No __manifest__.py found in module root: {input_path}")
|
|
15
|
+
return input_path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def list_python_files(module_root: Path) -> list[Path]:
|
|
19
|
+
"""Quét và trả về danh sách tất cả file .py trong module (đệ quy).
|
|
20
|
+
|
|
21
|
+
Ví dụ: trả về ``[models/__init__.py, models/sale_order.py, ...]``.
|
|
22
|
+
"""
|
|
23
|
+
files: list[Path] = []
|
|
24
|
+
for path in module_root.rglob("*.py"):
|
|
25
|
+
if path.is_file():
|
|
26
|
+
files.append(path)
|
|
27
|
+
return sorted(files)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _glob_matches(module_root: Path, patterns: list[str]) -> set[Path]:
|
|
5
|
+
"""Trả về tập file dưới ``module_root`` khớp với bất kỳ glob nào.
|
|
6
|
+
|
|
7
|
+
Dùng ``Path.glob`` (hỗ trợ wildcard đệ quy ``**``), nên một pattern duy nhất
|
|
8
|
+
``models/**/*.py`` khớp cả file ngay trong ``models`` lẫn thư mục con lồng sâu.
|
|
9
|
+
"""
|
|
10
|
+
matched: set[Path] = set()
|
|
11
|
+
for pattern in patterns:
|
|
12
|
+
for path in module_root.glob(pattern):
|
|
13
|
+
if path.is_file():
|
|
14
|
+
matched.add(path)
|
|
15
|
+
return matched
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def select_targets(
|
|
19
|
+
files: list[Path],
|
|
20
|
+
module_root: Path,
|
|
21
|
+
include_patterns: list[str],
|
|
22
|
+
exclude_patterns: list[str],
|
|
23
|
+
) -> tuple[list[Path], list[Path]]:
|
|
24
|
+
"""Chia ``files`` thành (selected, skipped) theo glob include/exclude.
|
|
25
|
+
|
|
26
|
+
Ví dụ: include=['models/**/*.py'], exclude=['**/__init__.py'] ->
|
|
27
|
+
``models/sale_order.py`` vào selected, ``models/__init__.py`` vào skipped.
|
|
28
|
+
"""
|
|
29
|
+
included = _glob_matches(module_root, include_patterns)
|
|
30
|
+
excluded = _glob_matches(module_root, exclude_patterns)
|
|
31
|
+
|
|
32
|
+
candidate_set = set(files)
|
|
33
|
+
selected = sorted((included & candidate_set) - excluded)
|
|
34
|
+
|
|
35
|
+
selected_lookup = set(selected)
|
|
36
|
+
skipped = [path for path in files if path not in selected_lookup]
|
|
37
|
+
return selected, skipped
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from shutil import copytree
|
|
3
|
+
|
|
4
|
+
from jprotect.config.schema import ProtectConfig
|
|
5
|
+
from jprotect.core.compile.cython_backend import compile_protected_files
|
|
6
|
+
from jprotect.core.discovery import discover_module_root, list_python_files
|
|
7
|
+
from jprotect.core.filtering import select_targets
|
|
8
|
+
from jprotect.core.report import build_report, write_manifest_json
|
|
9
|
+
from jprotect.core.safety import assert_safe_output_path
|
|
10
|
+
from jprotect.core.transform.transformer import transform_output_module
|
|
11
|
+
from jprotect.errors import SafetyError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _prepare_output(config: ProtectConfig, module_root: Path) -> Path:
|
|
15
|
+
"""Chuẩn bị thư mục output: giữ nguyên nếu inplace, copy nếu dist.
|
|
16
|
+
|
|
17
|
+
Ví dụ: dist mode sẽ copy toàn bộ module sang ``output_path`` (báo lỗi nếu đã tồn tại).
|
|
18
|
+
"""
|
|
19
|
+
if config.mode == "inplace":
|
|
20
|
+
return module_root
|
|
21
|
+
|
|
22
|
+
output_root = config.output_path
|
|
23
|
+
if output_root.exists():
|
|
24
|
+
raise SafetyError(f"Output path already exists: {output_root}")
|
|
25
|
+
|
|
26
|
+
copytree(module_root, output_root)
|
|
27
|
+
return output_root
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def run_pipeline(config: ProtectConfig) -> dict:
|
|
31
|
+
"""Điều phối toàn bộ pipeline: validate, quét, lọc, transform, compile và report.
|
|
32
|
+
|
|
33
|
+
Ví dụ: dry-run chỉ quét và trả report; chạy thật sẽ tạo output module,
|
|
34
|
+
transform method và (nếu có ``--compile``) build ra .so/.pyd.
|
|
35
|
+
"""
|
|
36
|
+
module_root = discover_module_root(config.input_path)
|
|
37
|
+
assert_safe_output_path(module_root, config.output_path, config.mode)
|
|
38
|
+
|
|
39
|
+
files = list_python_files(module_root)
|
|
40
|
+
selected, skipped = select_targets(
|
|
41
|
+
files=files,
|
|
42
|
+
module_root=module_root,
|
|
43
|
+
include_patterns=config.include_patterns,
|
|
44
|
+
exclude_patterns=config.exclude_patterns,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if config.dry_run:
|
|
48
|
+
output_root = config.output_path if config.mode == "dist" else module_root
|
|
49
|
+
report = build_report(
|
|
50
|
+
module_root=module_root,
|
|
51
|
+
output_root=output_root,
|
|
52
|
+
selected=selected,
|
|
53
|
+
skipped=skipped,
|
|
54
|
+
dry_run=True,
|
|
55
|
+
mode=config.mode,
|
|
56
|
+
)
|
|
57
|
+
report["manifest_path"] = None
|
|
58
|
+
return report
|
|
59
|
+
|
|
60
|
+
output_root = _prepare_output(config, module_root)
|
|
61
|
+
|
|
62
|
+
# Phase 2: tách thân method sang _protected và viết lại method công khai (delegation).
|
|
63
|
+
transform_result = transform_output_module(output_root, module_root, selected)
|
|
64
|
+
|
|
65
|
+
# Phase 3: compile các file đã tách thành nhị phân (tùy chọn qua --compile).
|
|
66
|
+
compiled_artifacts: list[Path] = []
|
|
67
|
+
if config.compile:
|
|
68
|
+
compiled_artifacts = compile_protected_files(transform_result.protected_files, output_root)
|
|
69
|
+
|
|
70
|
+
report = build_report(
|
|
71
|
+
module_root=module_root,
|
|
72
|
+
output_root=output_root,
|
|
73
|
+
selected=selected,
|
|
74
|
+
skipped=skipped,
|
|
75
|
+
dry_run=False,
|
|
76
|
+
mode=config.mode,
|
|
77
|
+
transformed=transform_result.protected_files,
|
|
78
|
+
compiled=compiled_artifacts,
|
|
79
|
+
compiled_enabled=config.compile,
|
|
80
|
+
)
|
|
81
|
+
manifest_path = write_manifest_json(report, output_root)
|
|
82
|
+
report["manifest_path"] = str(manifest_path)
|
|
83
|
+
return report
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Các hàm hỗ trợ hậu xử lý (placeholder cho Phase 3)."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def normalize_artifact_names(artifacts: list[Path]) -> list[Path]:
|
|
7
|
+
"""Chuẩn hóa tên file artifact sau khi compile (sẽ làm ở Phase 3).
|
|
8
|
+
|
|
9
|
+
Ví dụ: hiện tại trả về nguyên danh sách, chưa đổi tên.
|
|
10
|
+
"""
|
|
11
|
+
return artifacts
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def cleanup_temp_files(paths: list[Path]) -> None:
|
|
15
|
+
"""Xóa các file trung gian sinh ra trong quá trình build (Phase 3)."""
|
|
16
|
+
for _path in paths:
|
|
17
|
+
# Placeholder: chính sách dọn dẹp sẽ được bổ sung ở Phase 3.
|
|
18
|
+
pass
|
jprotect/core/report.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def build_report(
|
|
6
|
+
module_root: Path,
|
|
7
|
+
output_root: Path,
|
|
8
|
+
selected: list[Path],
|
|
9
|
+
skipped: list[Path],
|
|
10
|
+
dry_run: bool,
|
|
11
|
+
mode: str,
|
|
12
|
+
transformed: list[Path] | None = None,
|
|
13
|
+
compiled: list[Path] | None = None,
|
|
14
|
+
compiled_enabled: bool = False,
|
|
15
|
+
) -> dict:
|
|
16
|
+
"""Tạo dict report tổng hợp kết quả quét, lọc, transform và compile.
|
|
17
|
+
|
|
18
|
+
Ví dụ: trả về dict gồm số file selected/skipped/transformed/compiled và
|
|
19
|
+
danh sách đường dẫn tương ứng để ghi ra manifest.
|
|
20
|
+
"""
|
|
21
|
+
transformed = transformed or []
|
|
22
|
+
compiled = compiled or []
|
|
23
|
+
return {
|
|
24
|
+
"module_root": str(module_root),
|
|
25
|
+
"output_root": str(output_root),
|
|
26
|
+
"mode": mode,
|
|
27
|
+
"dry_run": dry_run,
|
|
28
|
+
"selected_count": len(selected),
|
|
29
|
+
"skipped_count": len(skipped),
|
|
30
|
+
"transformed_count": len(transformed),
|
|
31
|
+
"compiled_enabled": compiled_enabled,
|
|
32
|
+
"compiled_count": len(compiled),
|
|
33
|
+
"selected_files": [str(p) for p in selected],
|
|
34
|
+
"skipped_files": [str(p) for p in skipped],
|
|
35
|
+
"transformed_files": [str(p) for p in transformed],
|
|
36
|
+
"compiled_artifacts": [str(p) for p in compiled],
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def write_manifest_json(report: dict, output_root: Path) -> Path:
|
|
41
|
+
"""Ghi report ra file protection-manifest.json trong thư mục output.
|
|
42
|
+
|
|
43
|
+
Ví dụ: tạo ``<output_root>/protection-manifest.json``.
|
|
44
|
+
"""
|
|
45
|
+
manifest_path = output_root / "protection-manifest.json"
|
|
46
|
+
manifest_path.write_text(json.dumps(report, indent=2), encoding="utf-8")
|
|
47
|
+
return manifest_path
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def print_summary(report: dict) -> None:
|
|
51
|
+
"""In tóm tắt kết quả ra console cho người dùng dễ đọc."""
|
|
52
|
+
print("=== jProtect Summary ===")
|
|
53
|
+
print(f"Module root : {report['module_root']}")
|
|
54
|
+
print(f"Output root : {report['output_root']}")
|
|
55
|
+
print(f"Mode : {report['mode']}")
|
|
56
|
+
print(f"Dry run : {report['dry_run']}")
|
|
57
|
+
print(f"Selected : {report['selected_count']}")
|
|
58
|
+
print(f"Skipped : {report['skipped_count']}")
|
|
59
|
+
if not report['dry_run']:
|
|
60
|
+
print(f"Transformed : {report.get('transformed_count', 0)}")
|
|
61
|
+
print(f"Compiled : {report.get('compiled_count', 0)}")
|
jprotect/core/safety.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from jprotect.errors import SafetyError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
PROTECTED_NAMES = {"", ".", "..", "/", "\\"}
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def assert_no_destructive_target(path: Path) -> None:
|
|
10
|
+
"""Chặn các đường dẫn nguy hiểm có thể gây xóa nhầm (root, '.', '..').
|
|
11
|
+
|
|
12
|
+
Ví dụ: path là '/', '.', '..' sẽ ném SafetyError.
|
|
13
|
+
"""
|
|
14
|
+
path_str = str(path).strip()
|
|
15
|
+
if path_str in PROTECTED_NAMES:
|
|
16
|
+
raise SafetyError(f"Unsafe path: {path}")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def assert_safe_output_path(input_path: Path, output_path: Path, mode: str) -> None:
|
|
20
|
+
"""Đảm bảo output an toàn và không nằm bên trong input khi ở chế độ dist.
|
|
21
|
+
|
|
22
|
+
Ví dụ: input=/addons/sale_ext, output=/addons/sale_ext/dist -> bị chặn
|
|
23
|
+
(output nằm trong input).
|
|
24
|
+
"""
|
|
25
|
+
assert_no_destructive_target(output_path)
|
|
26
|
+
|
|
27
|
+
if mode == "dist":
|
|
28
|
+
# Chặn việc tạo output bên trong cây thư mục input.
|
|
29
|
+
input_parts = input_path.resolve().parts
|
|
30
|
+
output_parts = output_path.resolve().parts
|
|
31
|
+
if output_parts[: len(input_parts)] == input_parts:
|
|
32
|
+
raise SafetyError("Output path must not be inside input path in dist mode.")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Các implementation cho bước transform của jProtect."""
|