MB-X Bilibili Pipeline
6 days ago c1a8a80d6e12eedb07a3d6ac924163204c603cd5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#!/usr/bin/env python3
"""Minimal native-messaging boundary for the generic article/image collector."""
 
from __future__ import annotations
 
import json
import struct
import sys
from pathlib import Path
from typing import Any, BinaryIO, Mapping
 
import bili_article_image_collector as collector
 
 
MAX_REQUEST_BYTES = 16 * 1024 * 1024
MAX_RESPONSE_BYTES = 1024 * 1024
PROJECT_ROOT = Path(__file__).resolve().parents[2]
 
 
def _exact_keys(value: Any, expected: set[str]) -> Mapping[str, Any]:
    if not isinstance(value, Mapping) or set(value) != expected:
        raise collector.CollectorError("E_HOST_SCHEMA", "Native Host request keys differ.", safety=True)
    collector._reject_secrets(value)
    return value
 
 
def _project_path(value: Any, *, required: bool) -> Path | None:
    if value is None and not required:
        return None
    if not isinstance(value, str) or not value.strip():
        raise collector.CollectorError("E_HOST_PATH", "Native Host path is invalid.", safety=True)
    path = Path(value)
    path = (path if path.is_absolute() else PROJECT_ROOT / path).resolve(strict=False)
    if not collector._within(path, PROJECT_ROOT):
        raise collector.CollectorError("E_HOST_PATH", "Native Host path escapes the project root.", safety=True)
    collector._safe_existing_chain(path, allow_missing_leaf=not required)
    if required:
        collector._ordinary_file(path)
    return path
 
 
def run_request(raw: Any) -> tuple[int, dict[str, Any]]:
    try:
        request = _exact_keys(raw, {"schema_version", "action", "config_path", "capture_path", "terminal_path"})
        if request["schema_version"] != 1:
            raise collector.CollectorError("E_HOST_SCHEMA", "Native Host schema_version differs.", safety=True)
        action = request["action"]
        if action not in {"validate_capture", "collect", "verify"}:
            raise collector.CollectorError("E_HOST_ACTION", "Native Host action is unsupported.", safety=True)
        config_path = _project_path(request["config_path"], required=True)
        capture_required = action != "verify"
        capture_path = _project_path(request["capture_path"], required=capture_required)
        terminal_path = _project_path(request["terminal_path"], required=False)
        if not capture_required and capture_path is not None:
            raise collector.CollectorError("E_HOST_SCHEMA", "verify forbids capture_path.", safety=True)
        argv = ["--config", str(config_path)]
        if action == "validate_capture":
            if terminal_path is not None:
                raise collector.CollectorError("E_HOST_SCHEMA", "validate_capture forbids terminal_path.", safety=True)
            argv.extend(["validate-capture", "--capture", str(capture_path)])
        elif action == "collect":
            argv.extend(["collect", "--capture", str(capture_path)])
            if terminal_path is not None:
                argv.extend(["--terminal", str(terminal_path)])
        else:
            argv.append("verify")
            if terminal_path is not None:
                argv.extend(["--terminal", str(terminal_path)])
        return collector.run(argv)
    except collector.CollectorError as exc:
        return (3 if exc.safety else 2), {
            "schema_version": 1,
            "status": "SAFETY_STOP" if exc.safety else "INPUT_ERROR",
            "error_code": exc.code,
            "message": exc.message,
            "mutation_count": 0,
        }
    except Exception:
        return 1, {"schema_version": 1, "status": "INTERNAL_ERROR", "error_code": "E_INTERNAL", "mutation_count": 0}
 
 
def _read_frame(stream: BinaryIO) -> Any:
    header = stream.read(4)
    if len(header) != 4:
        raise EOFError
    size = struct.unpack("<I", header)[0]
    if size < 2 or size > MAX_REQUEST_BYTES:
        raise collector.CollectorError("E_HOST_FRAME", "Native Host request frame size is invalid.", safety=True)
    payload = stream.read(size)
    if len(payload) != size:
        raise collector.CollectorError("E_HOST_FRAME", "Native Host request frame is truncated.", safety=True)
    return json.loads(payload.decode("utf-8", errors="strict"))
 
 
def _write_frame(stream: BinaryIO, value: Mapping[str, Any]) -> None:
    payload = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
    if len(payload) > MAX_RESPONSE_BYTES:
        payload = b'{"error_code":"E_HOST_RESPONSE_LIMIT","mutation_count":0,"schema_version":1,"status":"INTERNAL_ERROR"}'
    stream.write(struct.pack("<I", len(payload)))
    stream.write(payload)
    stream.flush()
 
 
def main() -> int:
    try:
        request = _read_frame(sys.stdin.buffer)
        code, result = run_request(request)
    except EOFError:
        return 0
    except Exception:
        code, result = 3, {"schema_version": 1, "status": "SAFETY_STOP", "error_code": "E_HOST_FRAME", "mutation_count": 0}
    _write_frame(sys.stdout.buffer, result)
    return code
 
 
if __name__ == "__main__":
    raise SystemExit(main())