MainShortcuts2 2.7.3__tar.gz → 2.8.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.
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/PKG-INFO +2 -6
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/pyproject.toml +3 -2
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/_module_info.py +1 -1
- mainshortcuts2-2.8.0/src/MainShortcuts2/_ms2dat_auto.py +28 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/core.py +16 -5
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/core_config.py +9 -14
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/ex/aiohttp_ex/web/__init__.py +94 -5
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/java_ext/__init__.py +10 -4
- mainshortcuts2-2.8.0/src/MainShortcuts2/ms2dat1.py +549 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/ms2hash.py +16 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/types.py +92 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/utils.py +106 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/README.md +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/__init__.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/__main__.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/_any2json_regs.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/_ms2app_regs.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/advanced.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/any2json.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/api/base.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/api/gigachat.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/api/github.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/api/russian_trusted_root_ca_pem.crt +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/api/russian_trusted_sub_ca_pem.crt +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/api/webdav.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/cfg.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/dict.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/dir.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/ex/__init__.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/ex/aiohttp_ex/web/__main__.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/ex/datetime_ex.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/ex/pathlib_ex.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/ex/pil_ex.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/ex/psutil_ex.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/ex/sqlite_ex.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/ex/urlparse_ex.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/file.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/gui_scripts/__init__.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/gui_scripts/ms2_hash_gen.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/gui_scripts/utils.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/java_ext/ms2ext.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/json.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/linux.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/list.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/ms2app.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/path.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/proc.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/regex.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/special_chars.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/sql/_sql_base.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/sql/postgresql.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/sql/sqlite.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/str.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/term.py +0 -0
- {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/win.py +0 -0
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MainShortcuts2
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.8.0
|
|
4
4
|
Summary: Сокращение и улучшение функций + консольные утилиты
|
|
5
5
|
Author: MainPlay TG
|
|
6
6
|
Author-email: xbox.roman6666666666@gmail.com
|
|
7
|
-
Requires-Python: >=3.
|
|
7
|
+
Requires-Python: >=3.10,<4.0
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
9
|
-
Classifier: Programming Language :: Python :: 3.6
|
|
10
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
13
9
|
Classifier: Programming Language :: Python :: 3.10
|
|
14
10
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
11
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
|
-
version = "2.
|
|
2
|
+
version = "2.8.0"
|
|
3
3
|
name = "MainShortcuts2"
|
|
4
4
|
description = "Сокращение и улучшение функций + консольные утилиты"
|
|
5
5
|
authors = ["MainPlay TG <xbox.roman6666666666@gmail.com>"]
|
|
@@ -13,6 +13,7 @@ packages = [
|
|
|
13
13
|
ms2-app = "MainShortcuts2.ms2app:main"
|
|
14
14
|
ms2-hash_check = "MainShortcuts2.ms2hash:hash_check"
|
|
15
15
|
ms2-hash_gen = "MainShortcuts2.ms2hash:hash_gen"
|
|
16
|
+
ms2-hash_java = "MainShortcuts2.ms2hash:run_java_ext"
|
|
16
17
|
ms2-import_example = "MainShortcuts2.__main__:import_example"
|
|
17
18
|
ms2-ln = "MainShortcuts2.__main__:ln"
|
|
18
19
|
ms2-which_real = "MainShortcuts2.__main__:which_real"
|
|
@@ -21,7 +22,7 @@ nginx-reload = "MainShortcuts2.__main__:nginx_reload"
|
|
|
21
22
|
nginx-restart = "MainShortcuts2.__main__:nginx_restart"
|
|
22
23
|
|
|
23
24
|
[tool.poetry.dependencies]
|
|
24
|
-
python = "^3.
|
|
25
|
+
python = "^3.10"
|
|
25
26
|
|
|
26
27
|
[build-system]
|
|
27
28
|
build-backend = "poetry.core.masonry.api"
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
name = 'mainshortcuts2'
|
|
2
|
-
version = '2.
|
|
2
|
+
version = '2.8.0'
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
from . import ms2dat1
|
|
3
|
+
from io import BytesIO
|
|
4
|
+
# Использовать последнюю версию для сохранения
|
|
5
|
+
dump = ms2dat1.dump
|
|
6
|
+
dumps = ms2dat1.dumps
|
|
7
|
+
write_file = ms2dat1.write_file
|
|
8
|
+
# Авто определение версии при загрузке
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def load(file: typing.BinaryIO, **kw):
|
|
12
|
+
"""Загрузить объект из IO"""
|
|
13
|
+
header = ms2dat1.FileHeader.from_file(file)
|
|
14
|
+
if header.version == 1:
|
|
15
|
+
return ms2dat1.load(file, _h=header, **kw)
|
|
16
|
+
raise Exception(f"Unsupported version {header.version}")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def loads(data: bytes, **kw):
|
|
20
|
+
"""Загрузить объект из `bytes`"""
|
|
21
|
+
with BytesIO(data) as f:
|
|
22
|
+
return load(f, **kw)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def read_file(path, **kw):
|
|
26
|
+
"""Загрузить объект из локального файла"""
|
|
27
|
+
with open(path, "rb") as f:
|
|
28
|
+
return load(f, **kw)
|
|
@@ -192,29 +192,40 @@ class MS2:
|
|
|
192
192
|
return cls(**kw)
|
|
193
193
|
|
|
194
194
|
@property
|
|
195
|
-
def now(self)
|
|
195
|
+
def now(self):
|
|
196
196
|
"""Текущее локальное время (`timestamp`)"""
|
|
197
197
|
return time()
|
|
198
198
|
|
|
199
199
|
@property
|
|
200
|
-
def now_dt(self)
|
|
200
|
+
def now_dt(self):
|
|
201
201
|
"""Текущее локальное время (`datetime`)"""
|
|
202
202
|
return datetime.fromtimestamp(time())
|
|
203
203
|
|
|
204
204
|
@property
|
|
205
|
-
def utcnow(self)
|
|
205
|
+
def utcnow(self):
|
|
206
206
|
"""Текущее время по UTC (`timestamp`)"""
|
|
207
207
|
return self.utcnow_dt.timestamp()
|
|
208
208
|
if timezone is None:
|
|
209
209
|
@property
|
|
210
|
-
def utcnow_dt(self)
|
|
210
|
+
def utcnow_dt(self):
|
|
211
211
|
"""Текущее время по UTC (`datetime`)"""
|
|
212
212
|
return datetime.utcfromtimestamp(time())
|
|
213
213
|
else:
|
|
214
214
|
@property
|
|
215
|
-
def utcnow_dt(self)
|
|
215
|
+
def utcnow_dt(self):
|
|
216
216
|
"""Текущее время по UTC (`datetime`)"""
|
|
217
217
|
return datetime.fromtimestamp(time(), timezone.utc)
|
|
218
218
|
|
|
219
|
+
@cached_property
|
|
220
|
+
def ms2dat_v1(self):
|
|
221
|
+
from . import ms2dat1
|
|
222
|
+
return ms2dat1
|
|
223
|
+
|
|
224
|
+
@cached_property
|
|
225
|
+
def ms2dat(self):
|
|
226
|
+
"""Авто выбор при загрузке, последняя версия при сохранении"""
|
|
227
|
+
from . import _ms2dat_auto
|
|
228
|
+
return _ms2dat_auto
|
|
229
|
+
|
|
219
230
|
|
|
220
231
|
ms = MS2()
|
|
@@ -43,11 +43,7 @@ class CoreConfig(cfg):
|
|
|
43
43
|
|
|
44
44
|
def _cu_thread(self):
|
|
45
45
|
try:
|
|
46
|
-
|
|
47
|
-
url = "https://github.com/MainPlay-TG/MainShortcuts2.py/raw/refs/heads/master/src/MainShortcuts2/_module_info.py"
|
|
48
|
-
with requests.get(url) as resp:
|
|
49
|
-
resp.raise_for_status()
|
|
50
|
-
version = self._parse_version(resp.text)
|
|
46
|
+
version = self._get_github_version()
|
|
51
47
|
if version != _module_info.version:
|
|
52
48
|
mini_log("New MainShortcuts2 version available: %s", version)
|
|
53
49
|
mini_log("Please update by running:")
|
|
@@ -57,13 +53,12 @@ class CoreConfig(cfg):
|
|
|
57
53
|
except:
|
|
58
54
|
return
|
|
59
55
|
|
|
60
|
-
def
|
|
61
|
-
import
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
return ast.literal_eval(node.value)
|
|
56
|
+
def _get_github_version(self) -> str:
|
|
57
|
+
from MainShortcuts2.api.github import Client
|
|
58
|
+
gh = Client.from_env(True)
|
|
59
|
+
rel = gh.releases.get_latest("MainPlay-TG", "MainShortcuts2.py")
|
|
60
|
+
for asset in rel.assets:
|
|
61
|
+
if asset.name == "changelog.json":
|
|
62
|
+
with gh.http.get(asset.browser_download_url) as resp:
|
|
63
|
+
return resp.json()["version"]
|
|
69
64
|
return _module_info.version
|
{mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/ex/aiohttp_ex/web/__init__.py
RENAMED
|
@@ -1,29 +1,41 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
1
4
|
import typing
|
|
5
|
+
import uuid
|
|
2
6
|
import warnings
|
|
3
7
|
from aiohttp import BasicAuth, hdrs, web
|
|
4
8
|
from aiohttp.typedefs import Handler
|
|
5
9
|
from aiohttp.web import FileResponse, Request, Response, StreamResponse
|
|
10
|
+
from datetime import datetime, timedelta
|
|
6
11
|
from logging import DEBUG
|
|
7
12
|
from MainShortcuts2 import ms
|
|
8
|
-
from pathlib import Path
|
|
13
|
+
from pathlib import Path, PurePath
|
|
9
14
|
warnings.filterwarnings("ignore", "Inheritance class .* from web.Application is discouraged", DeprecationWarning)
|
|
15
|
+
PATH_TYPES = os.PathLike, PurePath
|
|
10
16
|
|
|
11
17
|
|
|
12
18
|
def wrap_handler(app: "Application", handler: Handler):
|
|
19
|
+
"""Обернуть обработчик запросов"""
|
|
13
20
|
async def wrapper(req: Request) -> StreamResponse | typing.Any:
|
|
21
|
+
"""Обёртка с поддержкой ответов `int`, `ApiResult` и `os.PathLike`"""
|
|
14
22
|
try:
|
|
15
23
|
resp = await handler(req)
|
|
16
|
-
if isinstance(resp, int):
|
|
24
|
+
if isinstance(resp, int): # Код статуса
|
|
17
25
|
resp = Response(status=resp)
|
|
18
|
-
elif isinstance(resp,
|
|
26
|
+
elif isinstance(resp, ApiResult): # Результат API обработчика
|
|
27
|
+
resp = resp.make_resp(req.content_type)
|
|
28
|
+
elif isinstance(resp, PATH_TYPES): # Локальный файл
|
|
19
29
|
resp = FileResponse(resp)
|
|
30
|
+
except ApiResult as exc: # Результат API исключения
|
|
31
|
+
resp = exc.make_resp(req.content_type)
|
|
20
32
|
except Exception as exc:
|
|
21
33
|
app.logger.exception("Failed to process request", exc_info=exc)
|
|
22
34
|
resp = Response(status=500, reason="Internal server error")
|
|
23
35
|
if app.logger.isEnabledFor(DEBUG):
|
|
24
36
|
remote = req.remote or "unknown"
|
|
25
37
|
if "X-Real-IP" in req.headers:
|
|
26
|
-
remote += " (
|
|
38
|
+
remote += " (X-Real-IP: %s)" % req.headers["X-Real-IP"]
|
|
27
39
|
status = resp.status if isinstance(resp, StreamResponse) else repr(resp)
|
|
28
40
|
app.logger.debug("Request %s %s from %s, return %s", req.method, req.raw_path, remote, status)
|
|
29
41
|
return resp
|
|
@@ -97,7 +109,84 @@ def auth_basic(header=hdrs.AUTHORIZATION, filter: typing.Callable[[BasicAuth], b
|
|
|
97
109
|
return Response(status=400, headers=basic_unauthorized_headers) # Неправильный заголовок
|
|
98
110
|
if filter(auth):
|
|
99
111
|
return await func(req, auth) # Одобрено
|
|
100
|
-
return Response(status=403) # Запрещено
|
|
112
|
+
return Response(status=403, headers=basic_unauthorized_headers) # Запрещено
|
|
101
113
|
return Response(status=401, headers=basic_unauthorized_headers) # Заголовок отсутствует
|
|
102
114
|
return wrapper
|
|
103
115
|
return deco
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class ApiResult(BaseException):
|
|
119
|
+
"""Результат для вызова API. Может быть вызван как исключение"""
|
|
120
|
+
|
|
121
|
+
def __init__(self, result=None, status=200, **base_dict):
|
|
122
|
+
super().__init__("This exception should be handled as a response to the request")
|
|
123
|
+
self.base_dict = base_dict
|
|
124
|
+
self.result = result
|
|
125
|
+
self.status = status
|
|
126
|
+
|
|
127
|
+
def to_dict(self):
|
|
128
|
+
r = self.base_dict.copy()
|
|
129
|
+
r["ok"] = self.status < 400
|
|
130
|
+
if self.result is not None:
|
|
131
|
+
r["result"] = self.result
|
|
132
|
+
return r
|
|
133
|
+
|
|
134
|
+
def _serialize_ms2dat(self, obj, other_serializer=None):
|
|
135
|
+
"""Для форматов, не поддерживающих `bytes`, `datetime`, `timedelta` и `UUID`"""
|
|
136
|
+
if isinstance(obj, bytes): # Base64
|
|
137
|
+
return base64.b64encode(obj).decode("utf-8")
|
|
138
|
+
if isinstance(obj, datetime): # ISO
|
|
139
|
+
return obj.isoformat()
|
|
140
|
+
if isinstance(obj, timedelta): # секунды
|
|
141
|
+
return obj.total_seconds()
|
|
142
|
+
if isinstance(obj, uuid.UUID): # HEX
|
|
143
|
+
return str(obj)
|
|
144
|
+
if other_serializer is None: # Другого сериализатора нет
|
|
145
|
+
raise TypeError(f"Object of type {obj.__class__.__name__} is not serializable")
|
|
146
|
+
return other_serializer(obj)
|
|
147
|
+
|
|
148
|
+
def _to_json(self):
|
|
149
|
+
# Минимальный размер за счёт
|
|
150
|
+
# коротких разделителей
|
|
151
|
+
# и отсутвия \uXXXX кодов
|
|
152
|
+
return "application/json", json.dumps(
|
|
153
|
+
self.to_dict(),
|
|
154
|
+
default=self._serialize_ms2dat,
|
|
155
|
+
ensure_ascii=False,
|
|
156
|
+
separators=(",", ":"),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def _to_yaml(self):
|
|
160
|
+
import yaml
|
|
161
|
+
return "application/yaml", yaml.safe_dump(self.to_dict())
|
|
162
|
+
|
|
163
|
+
def _to_xml(self):
|
|
164
|
+
raise NotImplementedError()
|
|
165
|
+
|
|
166
|
+
def _to_ms2dat1(self) -> tuple[str, bytes]:
|
|
167
|
+
from MainShortcuts2 import ms2dat1
|
|
168
|
+
return "application/x-ms2dat1", ms2dat1.dumps(self.to_dict())
|
|
169
|
+
|
|
170
|
+
def make_resp(self, content_type=None):
|
|
171
|
+
"""Создать ответ в указаном формате, по умолчанию JSON"""
|
|
172
|
+
try:
|
|
173
|
+
if content_type == "application/x-ms2dat1":
|
|
174
|
+
ctype, body = self._to_ms2dat1()
|
|
175
|
+
elif content_type == "application/yaml":
|
|
176
|
+
ctype, body = self._to_yaml()
|
|
177
|
+
else: # Если запрошенный формат не поддерживается
|
|
178
|
+
ctype, body = self._to_json()
|
|
179
|
+
except ImportError: # Если формат не поддерживается
|
|
180
|
+
ctype, body = self._to_json() # JSON должен сработать
|
|
181
|
+
# Клиент должен проверять формат ответа, на случай если он не соответствует запрошенному
|
|
182
|
+
if isinstance(body, str):
|
|
183
|
+
return web.Response(status=self.status, text=body, content_type=ctype)
|
|
184
|
+
return web.Response(status=self.status, body=body, content_type=ctype)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class ApiError(ApiResult):
|
|
188
|
+
"""Ошибка API. Можно вызывать как исключение"""
|
|
189
|
+
|
|
190
|
+
def __init__(self, message: str, status=400, **base_dict):
|
|
191
|
+
base_dict["message"] = message
|
|
192
|
+
super().__init__(status=status, **base_dict)
|
|
@@ -169,12 +169,15 @@ class JavaExtManager(ms.ObjectBase):
|
|
|
169
169
|
|
|
170
170
|
def iter_offline_versions(self):
|
|
171
171
|
"""Итерация версий из кэша"""
|
|
172
|
+
info_list: list[InfoDict] = []
|
|
172
173
|
for file in self.dir.iterdir():
|
|
173
174
|
if file.suffix == ".json" and file.is_file():
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
175
|
+
info_list.append(InfoDict(file.read_json()))
|
|
176
|
+
info_list.sort(key=lambda i: i.version_id)
|
|
177
|
+
for info in info_list[::-1]:
|
|
178
|
+
ver = self.get_version(info.version)
|
|
179
|
+
ver.__dict__["info"] = info # cached_property
|
|
180
|
+
yield ver
|
|
178
181
|
|
|
179
182
|
def iter_all_versions(self, online_count=github.MAX_PER_PAGE):
|
|
180
183
|
"""Итерация версий из кэша и с GitHub"""
|
|
@@ -212,10 +215,13 @@ class JavaExtManager(ms.ObjectBase):
|
|
|
212
215
|
|
|
213
216
|
def download_latest(self, count=1, download_jar=True):
|
|
214
217
|
"""Скачать последнюю версию (если не скачана)"""
|
|
218
|
+
results: list[JavaExtVersion] = []
|
|
215
219
|
for ver in self.iter_online_versions(count):
|
|
216
220
|
ver.info # Скачать файл JSON
|
|
217
221
|
if download_jar:
|
|
218
222
|
ver.jar_path # Скачать файл JAR
|
|
223
|
+
results.append(ver)
|
|
224
|
+
return results
|
|
219
225
|
|
|
220
226
|
|
|
221
227
|
class JavaExtVersion(ms.ObjectBase):
|
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import hashlib
|
|
3
|
+
import math
|
|
4
|
+
import secrets
|
|
5
|
+
import struct
|
|
6
|
+
import typing
|
|
7
|
+
import uuid
|
|
8
|
+
from io import BytesIO
|
|
9
|
+
from MainShortcuts2.utils import int_size_unsigned
|
|
10
|
+
HASH_NAMES = None, "sha256", "sha3-256", "sha512"
|
|
11
|
+
HASH_SIZES = 0, 32, 32, 64
|
|
12
|
+
MAGIC_HEAD = b"MS2D"
|
|
13
|
+
SPECIAL_TYPES = None, False, True, float("-inf"), float("inf"), float("nan")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def hash_data(hash_type: int, data: bytes):
|
|
17
|
+
"""Хеширование данных"""
|
|
18
|
+
if hash_type == 0:
|
|
19
|
+
return b""
|
|
20
|
+
return hashlib.new(HASH_NAMES[hash_type], data).digest()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def compress_data(compress_type: int, data: bytes):
|
|
24
|
+
"""Сжатие данных, применяется только если результат меньше исходных данных"""
|
|
25
|
+
if compress_type == 1:
|
|
26
|
+
import zlib
|
|
27
|
+
result = zlib.compress(data)
|
|
28
|
+
if len(result) < len(data):
|
|
29
|
+
return 1, result
|
|
30
|
+
elif compress_type == 2:
|
|
31
|
+
import bz2
|
|
32
|
+
result = bz2.compress(data)
|
|
33
|
+
if len(result) < len(data):
|
|
34
|
+
return 2, result
|
|
35
|
+
elif compress_type == 3:
|
|
36
|
+
import lzma # СУПЕР сжатие
|
|
37
|
+
result = lzma.compress(data, lzma.FORMAT_ALONE, preset=9 | lzma.PRESET_EXTREME)
|
|
38
|
+
if len(result) < len(data):
|
|
39
|
+
return 3, result
|
|
40
|
+
return 0, data
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def decompress_data(compress_type: int, data: bytes):
|
|
44
|
+
if compress_type == 0:
|
|
45
|
+
return data
|
|
46
|
+
if compress_type == 1:
|
|
47
|
+
import zlib
|
|
48
|
+
return zlib.decompress(data)
|
|
49
|
+
if compress_type == 2:
|
|
50
|
+
import bz2
|
|
51
|
+
return bz2.decompress(data)
|
|
52
|
+
if compress_type == 3:
|
|
53
|
+
import lzma
|
|
54
|
+
return lzma.decompress(data)
|
|
55
|
+
raise ValueError("Invalid compress type")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class FileHeader:
|
|
59
|
+
"""Подходит для всех версий"""
|
|
60
|
+
|
|
61
|
+
def __init__(self, version: int):
|
|
62
|
+
self.version = version
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def from_file(cls, f: typing.IO[bytes]):
|
|
66
|
+
if f.read(4) != MAGIC_HEAD:
|
|
67
|
+
raise ValueError("Invalid file header")
|
|
68
|
+
return cls(f.read(1)[0])
|
|
69
|
+
|
|
70
|
+
def build(self):
|
|
71
|
+
return bytes([*MAGIC_HEAD, self.version])
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class UnknownType(typing.NamedTuple):
|
|
75
|
+
typename: str
|
|
76
|
+
body: bytes
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class Reader:
|
|
80
|
+
"""v1"""
|
|
81
|
+
|
|
82
|
+
def __init__(self, ms2dat: "MS2Dat1", f: typing.IO[bytes]):
|
|
83
|
+
self.f = f
|
|
84
|
+
self.ms2dat = ms2dat
|
|
85
|
+
# MAGIC заголовок уже должен быть прочитан и проверен
|
|
86
|
+
# Флаги
|
|
87
|
+
flags = self._read1()
|
|
88
|
+
self.compress_type = flags >> 6 # 0b11000000 0-3
|
|
89
|
+
self.hash_type = flags >> 4 & 0b11 # 0b00110000 0-3
|
|
90
|
+
self.encrypted = flags >> 3 & 1 # 0b00001000 0-1
|
|
91
|
+
bodysize_size = flags & 0b111 # 0b00000111 0-7
|
|
92
|
+
# Пользовательские типы
|
|
93
|
+
self.custom_types: list[str] = []
|
|
94
|
+
for i in range(self._read1()):
|
|
95
|
+
self.custom_types.append(self._read(self._read1()).decode("utf-8"))
|
|
96
|
+
# Размер тела
|
|
97
|
+
self.body_size = int.from_bytes(self._read(bodysize_size), "big")
|
|
98
|
+
|
|
99
|
+
def _read(self, n: int):
|
|
100
|
+
buf = self.f.read(n)
|
|
101
|
+
if len(buf) != n:
|
|
102
|
+
raise ValueError(f"Expected {n} bytes, got {len(buf)}")
|
|
103
|
+
return buf
|
|
104
|
+
|
|
105
|
+
def _read1(self):
|
|
106
|
+
return self._read(1)[0]
|
|
107
|
+
|
|
108
|
+
def read_body(self, allow_unknown=False, verify=True) -> typing.Optional[typing.Any]:
|
|
109
|
+
raw = self._read(self.body_size)
|
|
110
|
+
if self.encrypted:
|
|
111
|
+
raw = self.ms2dat.decrypt_body(raw)
|
|
112
|
+
body = decompress_data(self.compress_type, raw)
|
|
113
|
+
if verify and self.hash_type:
|
|
114
|
+
reald = hash_data(self.hash_type, body)
|
|
115
|
+
saved = self._read(HASH_SIZES[self.hash_type])
|
|
116
|
+
if reald != saved:
|
|
117
|
+
raise ValueError("The file is corrupted")
|
|
118
|
+
with BytesIO(body) as buf:
|
|
119
|
+
# Чтение словаря
|
|
120
|
+
d = []
|
|
121
|
+
for i in range(int.from_bytes(buf.read(3), "big")):
|
|
122
|
+
d.append(self.decode_obj(buf, d, allow_unknown))
|
|
123
|
+
# Чтение тела
|
|
124
|
+
return self.decode_obj(buf, d, allow_unknown)
|
|
125
|
+
|
|
126
|
+
def decode_obj(self, buf: typing.IO[bytes], d: list, allow_unknown=False):
|
|
127
|
+
head = buf.read(1)[0]
|
|
128
|
+
typeid = head >> 4
|
|
129
|
+
sizesize = head & 0b1111
|
|
130
|
+
size = int.from_bytes(buf.read(sizesize), "big")
|
|
131
|
+
if typeid == 0: # Специальный тип
|
|
132
|
+
return SPECIAL_TYPES[size]
|
|
133
|
+
if typeid == 6: # dict
|
|
134
|
+
result = {}
|
|
135
|
+
for i in range(size):
|
|
136
|
+
k = self.decode_obj(buf, d, allow_unknown)
|
|
137
|
+
result[k] = self.decode_obj(buf, d, allow_unknown)
|
|
138
|
+
return result
|
|
139
|
+
if typeid in (7, 8, 9): # list, tuple, set
|
|
140
|
+
result = [self.decode_obj(buf, d, allow_unknown) for i in range(size)]
|
|
141
|
+
if typeid == 8:
|
|
142
|
+
return tuple(result)
|
|
143
|
+
if typeid == 9:
|
|
144
|
+
return set(result)
|
|
145
|
+
return result
|
|
146
|
+
if typeid == 12: # datetime + timezone
|
|
147
|
+
dt: datetime.datetime = self.decode_obj(buf, d)
|
|
148
|
+
td: datetime.timedelta = self.decode_obj(buf, d)
|
|
149
|
+
return dt.replace(tzinfo=datetime.timezone(td))
|
|
150
|
+
if typeid == 14: # Ссылка на словарь
|
|
151
|
+
return d[size]
|
|
152
|
+
if typeid == 15: # Пользовательский тип
|
|
153
|
+
ctypename = self.custom_types[buf.read(1)[0]]
|
|
154
|
+
ctype = self.ms2dat.custom_types.get(ctypename)
|
|
155
|
+
cbody = buf.read(size)
|
|
156
|
+
if ctype is None:
|
|
157
|
+
if allow_unknown:
|
|
158
|
+
return UnknownType(ctypename, cbody)
|
|
159
|
+
raise ValueError(f"Unknown custom type: {ctypename}")
|
|
160
|
+
return ctype.decode_obj(self, cbody, d)
|
|
161
|
+
body = buf.read(size)
|
|
162
|
+
if typeid == 1: # int
|
|
163
|
+
return int.from_bytes(body, "big")
|
|
164
|
+
if typeid == 2: # int
|
|
165
|
+
return -int.from_bytes(body, "big")
|
|
166
|
+
if typeid == 3: # float
|
|
167
|
+
return struct.unpack("d", body)[0]
|
|
168
|
+
if typeid == 4: # bytes
|
|
169
|
+
return body
|
|
170
|
+
if typeid == 5: # str
|
|
171
|
+
return body.decode("utf-8")
|
|
172
|
+
if typeid == 10: # datetime
|
|
173
|
+
return datetime.datetime.fromtimestamp(struct.unpack("d", body)[0])
|
|
174
|
+
if typeid == 11: # timedelta
|
|
175
|
+
return datetime.timedelta(seconds=struct.unpack("d", body)[0])
|
|
176
|
+
if typeid == 13: # UUID
|
|
177
|
+
return uuid.UUID(bytes=body)
|
|
178
|
+
raise Exception("Эта ошибка никогда не вылезет")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class Writer:
|
|
182
|
+
"""v1"""
|
|
183
|
+
|
|
184
|
+
def __init__(self, ms2dat: "MS2Dat1", f: typing.IO[bytes], obj):
|
|
185
|
+
self.f = f
|
|
186
|
+
self.ms2dat = ms2dat
|
|
187
|
+
self.obj = obj
|
|
188
|
+
self.obj_dict: list[bytes] = []
|
|
189
|
+
self.used_ctypes: list[CustomType] = []
|
|
190
|
+
|
|
191
|
+
def _write(self, data: bytes):
|
|
192
|
+
return self.f.write(data)
|
|
193
|
+
|
|
194
|
+
def _write1(self, byte: int):
|
|
195
|
+
return self._write(bytes([byte]))
|
|
196
|
+
|
|
197
|
+
def _addget_dict(self, typeid: int, size: int, body: bytes):
|
|
198
|
+
if typeid == 14:
|
|
199
|
+
# Если дана ссылка на словарь, то вернуть ее
|
|
200
|
+
return typeid, size, b""
|
|
201
|
+
# В словаре только уже собранные объекты
|
|
202
|
+
buf = bytes(self._build_obj(typeid, size, body))
|
|
203
|
+
if buf in self.obj_dict:
|
|
204
|
+
# Объект уже есть в словаре
|
|
205
|
+
return 14, self.obj_dict.index(buf), b""
|
|
206
|
+
if len(self.obj_dict) >= 0xffffff:
|
|
207
|
+
# Словарь переполнен
|
|
208
|
+
return typeid, size, body
|
|
209
|
+
# Добавить объект в словарь
|
|
210
|
+
self.obj_dict.append(buf)
|
|
211
|
+
return 14, len(self.obj_dict) - 1, b""
|
|
212
|
+
|
|
213
|
+
def _encode_obj(self, data, sort_keys: bool, use_dict: bool) -> tuple[int, int, bytes | bytearray]:
|
|
214
|
+
for m, n in enumerate(SPECIAL_TYPES):
|
|
215
|
+
if m < 3: # None, bool
|
|
216
|
+
if data is n:
|
|
217
|
+
return 0, m, b""
|
|
218
|
+
else: # float
|
|
219
|
+
if data == n:
|
|
220
|
+
return 0, m, b""
|
|
221
|
+
if isinstance(data, int):
|
|
222
|
+
typeid = 1
|
|
223
|
+
if data < 0:
|
|
224
|
+
data = -data
|
|
225
|
+
typeid = 2
|
|
226
|
+
size = int_size_unsigned(data)
|
|
227
|
+
return typeid, size, data.to_bytes(size, "big")
|
|
228
|
+
if isinstance(data, float):
|
|
229
|
+
if math.isnan(data):
|
|
230
|
+
return 0, 5, b""
|
|
231
|
+
result = struct.pack("d", data)
|
|
232
|
+
return 3, len(result), result
|
|
233
|
+
if isinstance(data, bytes):
|
|
234
|
+
if use_dict:
|
|
235
|
+
return self._addget_dict(4, len(data), data)
|
|
236
|
+
return 4, len(data), data
|
|
237
|
+
if isinstance(data, str):
|
|
238
|
+
result = data.encode("utf-8")
|
|
239
|
+
if use_dict:
|
|
240
|
+
return self._addget_dict(5, len(result), result)
|
|
241
|
+
return 5, len(result), result
|
|
242
|
+
if isinstance(data, dict):
|
|
243
|
+
items = list(data.items())
|
|
244
|
+
if sort_keys:
|
|
245
|
+
try:
|
|
246
|
+
items.sort()
|
|
247
|
+
except Exception:
|
|
248
|
+
pass
|
|
249
|
+
buf = bytearray()
|
|
250
|
+
for k, v in items:
|
|
251
|
+
buf.extend(self.encode_obj(k, sort_keys, use_dict))
|
|
252
|
+
buf.extend(self.encode_obj(v, sort_keys, use_dict))
|
|
253
|
+
return 6, len(items), buf
|
|
254
|
+
if isinstance(data, (list, tuple, set)) and not isinstance(data, UnknownType):
|
|
255
|
+
typeid = 7
|
|
256
|
+
if isinstance(data, tuple):
|
|
257
|
+
typeid = 8
|
|
258
|
+
elif isinstance(data, set):
|
|
259
|
+
typeid = 9
|
|
260
|
+
if sort_keys:
|
|
261
|
+
try:
|
|
262
|
+
data = sorted(data)
|
|
263
|
+
except Exception:
|
|
264
|
+
pass
|
|
265
|
+
buf = bytearray()
|
|
266
|
+
for v in data:
|
|
267
|
+
buf.extend(self.encode_obj(v, sort_keys, use_dict))
|
|
268
|
+
return typeid, len(data), buf
|
|
269
|
+
if isinstance(data, datetime.datetime):
|
|
270
|
+
if data.tzinfo is None:
|
|
271
|
+
result = struct.pack("d", data.timestamp())
|
|
272
|
+
return 10, len(result), result
|
|
273
|
+
td = data.tzinfo.utcoffset(data)
|
|
274
|
+
if td is None:
|
|
275
|
+
return self._encode_obj(data.replace(tzinfo=None))
|
|
276
|
+
buf = bytearray()
|
|
277
|
+
buf.extend(self.encode_obj(data.replace(tzinfo=None)))
|
|
278
|
+
buf.extend(self.encode_obj(td))
|
|
279
|
+
return 12, len(buf), buf
|
|
280
|
+
if isinstance(data, datetime.timedelta):
|
|
281
|
+
result = struct.pack("d", data.total_seconds())
|
|
282
|
+
return 11, len(result), result
|
|
283
|
+
if isinstance(data, uuid.UUID):
|
|
284
|
+
result = data.bytes
|
|
285
|
+
return 13, len(result), result
|
|
286
|
+
# Пользовательский тип
|
|
287
|
+
if isinstance(data, UnknownType):
|
|
288
|
+
handler = UnknownTypeHandler(self.ms2dat, data.typename)
|
|
289
|
+
else:
|
|
290
|
+
handler = self.ms2dat.find_handler(type(data))
|
|
291
|
+
if handler is None:
|
|
292
|
+
raise Exception(f"Unknown type: {type(data)}")
|
|
293
|
+
if handler in self.used_ctypes:
|
|
294
|
+
ctypeid = self.used_ctypes.index(handler)
|
|
295
|
+
else:
|
|
296
|
+
ctypeid = len(self.used_ctypes)
|
|
297
|
+
self.used_ctypes.append(handler)
|
|
298
|
+
body = handler.encode_obj(data, sort_keys)
|
|
299
|
+
result = 15, len(body), bytes([ctypeid]) + body
|
|
300
|
+
if use_dict and getattr(handler, "allow_dict", False):
|
|
301
|
+
return self._addget_dict(*result)
|
|
302
|
+
return result
|
|
303
|
+
|
|
304
|
+
def _build_obj(self, typeid: int, size: int, body: bytes):
|
|
305
|
+
sizesize = int_size_unsigned(size)
|
|
306
|
+
if sizesize > 0b1111:
|
|
307
|
+
raise Exception("Too big size")
|
|
308
|
+
buf = bytearray()
|
|
309
|
+
buf.append((typeid << 4) | sizesize)
|
|
310
|
+
buf.extend(size.to_bytes(sizesize, "big"))
|
|
311
|
+
buf.extend(body)
|
|
312
|
+
return buf
|
|
313
|
+
|
|
314
|
+
def encode_obj(self, data, sort_keys=False, use_dict=True):
|
|
315
|
+
typeid, size, body = self._encode_obj(data, sort_keys, use_dict)
|
|
316
|
+
return self._build_obj(typeid, size, body)
|
|
317
|
+
|
|
318
|
+
def write_all(self, compress_type=0, encrypted=0, hash_type=1, sort_keys=False, use_dict=True):
|
|
319
|
+
self._write(MAGIC_HEAD)
|
|
320
|
+
self._write1(1)
|
|
321
|
+
# Сохранение объекта
|
|
322
|
+
raw_obj = self.encode_obj(self.obj, sort_keys, use_dict)
|
|
323
|
+
# Тело
|
|
324
|
+
raw_body = bytearray()
|
|
325
|
+
raw_body.extend(len(self.obj_dict).to_bytes(3, "big"))
|
|
326
|
+
for i in self.obj_dict:
|
|
327
|
+
raw_body.extend(i)
|
|
328
|
+
raw_body.extend(raw_obj)
|
|
329
|
+
# Сжатие
|
|
330
|
+
compress_type, body = compress_data(compress_type, raw_body)
|
|
331
|
+
# Шифрование
|
|
332
|
+
encrypted = 1 if encrypted else 0 # Если дадут bool
|
|
333
|
+
if encrypted:
|
|
334
|
+
body = self.ms2dat.encrypt_body(body)
|
|
335
|
+
size = len(body)
|
|
336
|
+
sizesize = int_size_unsigned(size)
|
|
337
|
+
if sizesize > 0b111:
|
|
338
|
+
raise Exception("Too big body")
|
|
339
|
+
# Запись
|
|
340
|
+
# Флаги
|
|
341
|
+
self._write1((compress_type << 6) | (hash_type << 4) | (encrypted << 3) | sizesize)
|
|
342
|
+
# Пользовательские типы
|
|
343
|
+
self._write1(len(self.used_ctypes))
|
|
344
|
+
for i in self.used_ctypes:
|
|
345
|
+
name = i.typename.encode("utf-8")
|
|
346
|
+
self._write1(len(name))
|
|
347
|
+
self._write(name)
|
|
348
|
+
# Тело
|
|
349
|
+
self._write(size.to_bytes(sizesize, "big"))
|
|
350
|
+
self._write(body)
|
|
351
|
+
# Хеш
|
|
352
|
+
self._write(hash_data(hash_type, body))
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
class CustomType:
|
|
356
|
+
allow_dict: bool
|
|
357
|
+
handled_types: set[type]
|
|
358
|
+
typename: str
|
|
359
|
+
|
|
360
|
+
def __init__(self, ms2dat: "MS2Dat1"):
|
|
361
|
+
self.ms2dat = ms2dat
|
|
362
|
+
|
|
363
|
+
def __eq__(self, other):
|
|
364
|
+
if isinstance(other, CustomType):
|
|
365
|
+
return (self.ms2dat is other.ms2dat) and (self.typename == other.typename)
|
|
366
|
+
|
|
367
|
+
def encode_obj(self, data: object, sort_keys=False) -> bytes:
|
|
368
|
+
raise NotImplementedError()
|
|
369
|
+
|
|
370
|
+
def decode_obj(self, reader: Reader, body: bytes, d: list) -> object:
|
|
371
|
+
raise NotImplementedError()
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
class UnknownTypeHandler(CustomType):
|
|
375
|
+
def __init__(self, ms2dat: "MS2Dat1", typename: str):
|
|
376
|
+
super().__init__(ms2dat)
|
|
377
|
+
self.allow_dict = True
|
|
378
|
+
self.handled_types = set()
|
|
379
|
+
self.typename = typename
|
|
380
|
+
|
|
381
|
+
def encode_obj(self, data: UnknownType, sort_keys=False):
|
|
382
|
+
return data.body
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
class MS2Dat1:
|
|
386
|
+
VERSION = 1
|
|
387
|
+
COMPRESS_NONE = 0
|
|
388
|
+
COMPRESS_ZLIB = 1
|
|
389
|
+
COMPRESS_BZ2 = 2
|
|
390
|
+
COMPRESS_LZMA = 3
|
|
391
|
+
HASH_NONE = 0
|
|
392
|
+
HASH_SHA256 = 1
|
|
393
|
+
HASH_SHA3_256 = 2
|
|
394
|
+
HASH_SHA512 = 3
|
|
395
|
+
|
|
396
|
+
def __init__(self):
|
|
397
|
+
self._dump_kw = {}
|
|
398
|
+
self._load_kw = {}
|
|
399
|
+
self.custom_types: dict[str, CustomType] = {}
|
|
400
|
+
|
|
401
|
+
def set_allow_unknown(self, value: bool):
|
|
402
|
+
"""Разрешить загрузку неизвестных пользовательских типов? Такие объекты будут загружены в виде `UnknownType`"""
|
|
403
|
+
self._load_kw["allow_unknown"] = bool(value)
|
|
404
|
+
|
|
405
|
+
def set_compress(self, value: int):
|
|
406
|
+
"""Уровень сжатия (см. константы класса)"""
|
|
407
|
+
self._dump_kw["compress_type"] = value
|
|
408
|
+
|
|
409
|
+
def set_encrypt(self, value: bool):
|
|
410
|
+
"""Включить шифрование? Шифрование должно быть реализовано подклассом"""
|
|
411
|
+
self._dump_kw["encrypted"] = 1 if value else 0
|
|
412
|
+
|
|
413
|
+
def set_hash(self, value: int):
|
|
414
|
+
"""Алгоритм хеширования (см. константы класса)"""
|
|
415
|
+
self._dump_kw["hash_type"] = value
|
|
416
|
+
|
|
417
|
+
def set_sort_keys(self, value: bool):
|
|
418
|
+
"""Сортировать ключи `dict` и `set`?"""
|
|
419
|
+
self._dump_kw["sort_keys"] = bool(value)
|
|
420
|
+
|
|
421
|
+
def set_use_dict(self, value: bool):
|
|
422
|
+
"""Использовать словарь? Словарь уменьшает объём данных при наличии одинаковых объектов"""
|
|
423
|
+
self._dump_kw["use_dict"] = bool(value)
|
|
424
|
+
|
|
425
|
+
def set_verify(self, value: bool):
|
|
426
|
+
"""Проверять хеш данных (если есть) при загрузке?"""
|
|
427
|
+
self._load_kw["verify"] = bool(value)
|
|
428
|
+
|
|
429
|
+
def profile_fast(self):
|
|
430
|
+
"""Отключение сжатия, хеширования, сортировки, и словаря для быстрейшей работы"""
|
|
431
|
+
self.set_compress(self.COMPRESS_NONE)
|
|
432
|
+
self.set_hash(self.HASH_NONE)
|
|
433
|
+
self.set_sort_keys(False)
|
|
434
|
+
self.set_use_dict(False)
|
|
435
|
+
|
|
436
|
+
def profile_fastest(self):
|
|
437
|
+
"""Быстрый профиль + отключение проверки данных"""
|
|
438
|
+
self.profile_fast()
|
|
439
|
+
self.set_verify(False)
|
|
440
|
+
|
|
441
|
+
def profile_safe(self):
|
|
442
|
+
"""Лучшее хеширование"""
|
|
443
|
+
self.set_hash(self.HASH_SHA512)
|
|
444
|
+
self.set_verify(True)
|
|
445
|
+
|
|
446
|
+
def profile_smaller_size(self):
|
|
447
|
+
"""Максимальное сжатие, но медленная работа"""
|
|
448
|
+
self.set_compress(self.COMPRESS_LZMA)
|
|
449
|
+
|
|
450
|
+
def profile_minimum_size(self):
|
|
451
|
+
"""Максимальное сжатие без хеширования и с включенным словарём"""
|
|
452
|
+
self.set_compress(self.COMPRESS_LZMA)
|
|
453
|
+
self.set_hash(self.HASH_NONE)
|
|
454
|
+
self.set_use_dict(True)
|
|
455
|
+
|
|
456
|
+
def reg_custom_type(self, obj: CustomType, replace=False):
|
|
457
|
+
"""Зарегистрировать пользовательский тип"""
|
|
458
|
+
if (obj.typename in self.custom_types) and not replace:
|
|
459
|
+
raise Exception(f"Type {obj.typename} already registered")
|
|
460
|
+
self.custom_types[obj.typename] = obj
|
|
461
|
+
|
|
462
|
+
def reg_custom_type_deco(self, replace=False):
|
|
463
|
+
"""Регистрация пользовательского типа в виде декоратора над классом"""
|
|
464
|
+
def deco(cls: type[CustomType]):
|
|
465
|
+
self.reg_custom_type(cls(self), replace)
|
|
466
|
+
return cls
|
|
467
|
+
return deco
|
|
468
|
+
|
|
469
|
+
def find_handler(self, cls: type) -> CustomType | None:
|
|
470
|
+
"""Найти обработчик для типа данных (только пользовательские типы)"""
|
|
471
|
+
for i in self.custom_types.values():
|
|
472
|
+
if cls in i.handled_types:
|
|
473
|
+
return i
|
|
474
|
+
|
|
475
|
+
def decrypt_body(self, data: bytes) -> bytes:
|
|
476
|
+
"""Расшифровать тело. Должен быть переопределен для работы"""
|
|
477
|
+
raise NotImplementedError("This class cannot work with encryption")
|
|
478
|
+
|
|
479
|
+
def encrypt_body(self, data: bytes) -> bytes:
|
|
480
|
+
"""Зашифровать тело. Должен быть переопределен для работы"""
|
|
481
|
+
raise NotImplementedError("This class cannot work with encryption")
|
|
482
|
+
|
|
483
|
+
def dump(self, obj, file: typing.BinaryIO, **kw):
|
|
484
|
+
"""Сохранить объект в IO"""
|
|
485
|
+
writer = Writer(self, file, obj)
|
|
486
|
+
for k, v in self._dump_kw.items():
|
|
487
|
+
kw.setdefault(k, v)
|
|
488
|
+
writer.write_all(**kw)
|
|
489
|
+
|
|
490
|
+
def dumps(self, obj, **kw):
|
|
491
|
+
"""Сохранить объект в `bytes`"""
|
|
492
|
+
with BytesIO() as f:
|
|
493
|
+
self.dump(obj, f, **kw)
|
|
494
|
+
return f.getvalue()
|
|
495
|
+
|
|
496
|
+
def load(self, file: typing.BinaryIO, *, _h: FileHeader = None, **kw):
|
|
497
|
+
"""Загрузить объект из IO"""
|
|
498
|
+
if _h is None:
|
|
499
|
+
_h = FileHeader.from_file(file)
|
|
500
|
+
if _h.version != self.VERSION:
|
|
501
|
+
raise Exception(f"Unsupported version {_h.version}")
|
|
502
|
+
reader = Reader(self, file)
|
|
503
|
+
for k, v in self._load_kw.items():
|
|
504
|
+
kw.setdefault(k, v)
|
|
505
|
+
return reader.read_body(**kw)
|
|
506
|
+
|
|
507
|
+
def loads(self, data: bytes, **kw):
|
|
508
|
+
"""Загрузить объект из `bytes`"""
|
|
509
|
+
with BytesIO(data) as f:
|
|
510
|
+
return self.load(f, **kw)
|
|
511
|
+
|
|
512
|
+
def read_file(self, path, **kw):
|
|
513
|
+
"""Загрузить объект из локального файла"""
|
|
514
|
+
with open(path, "rb") as f:
|
|
515
|
+
return self.load(f, **kw)
|
|
516
|
+
|
|
517
|
+
def write_file(self, obj, path, **kw):
|
|
518
|
+
"""Сохранить объект в локальный файл"""
|
|
519
|
+
with open(path, "wb") as f:
|
|
520
|
+
self.dump(obj, f, **kw)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
class MS2Dat1EncryptExample(MS2Dat1):
|
|
524
|
+
"""Пример реализации шифрования через XOR. Ненадёжное шифрование!"""
|
|
525
|
+
|
|
526
|
+
def __init__(self, key: bytes):
|
|
527
|
+
super().__init__()
|
|
528
|
+
self.encrypt_key = key
|
|
529
|
+
|
|
530
|
+
@classmethod
|
|
531
|
+
def create_with_random_key(cls, keysize=32):
|
|
532
|
+
return cls(secrets.token_bytes(keysize))
|
|
533
|
+
|
|
534
|
+
def decrypt_body(self, data: bytes):
|
|
535
|
+
"""Зашифровать/расшифровать данные, используя XOR-шифрование"""
|
|
536
|
+
key = self.encrypt_key
|
|
537
|
+
lkey = len(key)
|
|
538
|
+
return bytes([b ^ key[i % lkey] for i, b in enumerate(data)])
|
|
539
|
+
encrypt_body = decrypt_body
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
inst = MS2Dat1()
|
|
543
|
+
"""Настройки по умолчанию"""
|
|
544
|
+
dump = inst.dump
|
|
545
|
+
dumps = inst.dumps
|
|
546
|
+
load = inst.load
|
|
547
|
+
loads = inst.loads
|
|
548
|
+
read_file = inst.read_file
|
|
549
|
+
write_file = inst.write_file
|
|
@@ -142,3 +142,19 @@ def hash_check(args: argparse.Namespace = None):
|
|
|
142
142
|
else:
|
|
143
143
|
print("Ошибка: файл " + shlex.quote(file) + " изменён")
|
|
144
144
|
completed.append(file)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def run_java_ext(argv: list[str] = None):
|
|
148
|
+
if argv is None:
|
|
149
|
+
argv = sys.argv[1:]
|
|
150
|
+
from MainShortcuts2 import java_ext
|
|
151
|
+
mgr = java_ext.JavaExtManager("MainPlay-TG", "MS2Hash.java")
|
|
152
|
+
ver = None
|
|
153
|
+
for i in mgr.iter_all_versions(1):
|
|
154
|
+
ver = i
|
|
155
|
+
break
|
|
156
|
+
if ver is None:
|
|
157
|
+
ms.utils.mini_log("Ошибка: не удалось найти версию MS2Hash. Проверьте соединение с GitHub")
|
|
158
|
+
sys.exit(1)
|
|
159
|
+
p = ver.run(argv, check=False)
|
|
160
|
+
sys.exit(p.returncode)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Различные объекты и исключения"""
|
|
2
|
+
from typing import NamedTuple
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
class Base:
|
|
@@ -468,6 +469,97 @@ class ThreadsFlag(BoolFlag):
|
|
|
468
469
|
self.value.remove(t)
|
|
469
470
|
|
|
470
471
|
|
|
472
|
+
class BitsTuple(NamedTuple):
|
|
473
|
+
b0: int
|
|
474
|
+
b1: int
|
|
475
|
+
b2: int
|
|
476
|
+
b3: int
|
|
477
|
+
b4: int
|
|
478
|
+
b5: int
|
|
479
|
+
b6: int
|
|
480
|
+
b7: int
|
|
481
|
+
|
|
482
|
+
def __int__(self):
|
|
483
|
+
return self.to_byte()
|
|
484
|
+
|
|
485
|
+
def __index__(self):
|
|
486
|
+
return self.to_byte()
|
|
487
|
+
# Возможны баги
|
|
488
|
+
|
|
489
|
+
def __and__(self, other):
|
|
490
|
+
return int(self) & other
|
|
491
|
+
|
|
492
|
+
def __or__(self, other):
|
|
493
|
+
return int(self) | other
|
|
494
|
+
|
|
495
|
+
def __xor__(self, other):
|
|
496
|
+
return int(self) ^ other
|
|
497
|
+
|
|
498
|
+
def __lshift__(self, other):
|
|
499
|
+
return int(self) << other
|
|
500
|
+
|
|
501
|
+
def __rshift__(self, other):
|
|
502
|
+
return int(self) >> other
|
|
503
|
+
|
|
504
|
+
def __invert__(self):
|
|
505
|
+
return (~int(self)) & 0xFF
|
|
506
|
+
|
|
507
|
+
def __rand__(self, other):
|
|
508
|
+
return other & int(self)
|
|
509
|
+
|
|
510
|
+
def __ror__(self, other):
|
|
511
|
+
return other | int(self)
|
|
512
|
+
|
|
513
|
+
def __rxor__(self, other):
|
|
514
|
+
return other ^ int(self)
|
|
515
|
+
|
|
516
|
+
def __rlshift__(self, other):
|
|
517
|
+
return other << int(self)
|
|
518
|
+
|
|
519
|
+
def __rrshift__(self, other):
|
|
520
|
+
return other >> int(self)
|
|
521
|
+
# ---
|
|
522
|
+
|
|
523
|
+
@classmethod
|
|
524
|
+
def from_byte(cls, b: int):
|
|
525
|
+
"""Разбить **один** байт на биты"""
|
|
526
|
+
return cls(*[(b >> i) & 1 for i in range(7, -1, -1)])
|
|
527
|
+
|
|
528
|
+
@classmethod
|
|
529
|
+
def from_bytes_iter(cls, b: bytes):
|
|
530
|
+
"""Разбить **несколько** байт на группы битов (итерация)"""
|
|
531
|
+
return map(cls.from_byte, b)
|
|
532
|
+
|
|
533
|
+
@classmethod
|
|
534
|
+
def from_bytes(cls, b: bytes):
|
|
535
|
+
"""Разбить **несколько** байт на группы битов"""
|
|
536
|
+
return list(cls.from_bytes_iter(b))
|
|
537
|
+
|
|
538
|
+
def to_byte(self):
|
|
539
|
+
"""Соединить биты в байт"""
|
|
540
|
+
b = 0
|
|
541
|
+
for i in self:
|
|
542
|
+
b = (b << 1) | i
|
|
543
|
+
return b
|
|
544
|
+
|
|
545
|
+
def replace(self, *args: bool | int, **kw: bool | int):
|
|
546
|
+
for n, bit in enumerate(args):
|
|
547
|
+
kw[f"b{n}"] = bit
|
|
548
|
+
fields = set(self._fields)
|
|
549
|
+
for k in list(kw):
|
|
550
|
+
if k not in fields:
|
|
551
|
+
raise ValueError(f"Invalid field: {k}, must be one of {sorted(fields)}")
|
|
552
|
+
kw[k] = 1 if kw[k] else 0
|
|
553
|
+
return self._replace(**kw)
|
|
554
|
+
|
|
555
|
+
def to_bool_list(self):
|
|
556
|
+
return list(map(bool, self))
|
|
557
|
+
|
|
558
|
+
@classmethod
|
|
559
|
+
def from_bool_list(cls, l):
|
|
560
|
+
return cls(*l)
|
|
561
|
+
|
|
562
|
+
|
|
471
563
|
COLORS = _COLORS()
|
|
472
564
|
Error401 = AccessDeniedError
|
|
473
565
|
Error403 = AccessDeniedError
|
|
@@ -1102,3 +1102,109 @@ def guess_checksum_alg(digest_len: int):
|
|
|
1102
1102
|
if digest_len == v:
|
|
1103
1103
|
result.add(k)
|
|
1104
1104
|
return result
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
def int_size_unsigned(n: int):
|
|
1108
|
+
"""Минимальное количество байт для неотрицательного `int`"""
|
|
1109
|
+
if n == 0:
|
|
1110
|
+
return 1
|
|
1111
|
+
return (n.bit_length() + 7) // 8
|
|
1112
|
+
|
|
1113
|
+
|
|
1114
|
+
def int_size_signed(n: int) -> int:
|
|
1115
|
+
"""Минимальное количество байт для `int` с поддержкой отрицательных"""
|
|
1116
|
+
if n == 0:
|
|
1117
|
+
return 1
|
|
1118
|
+
return (n.bit_length() + 8) // 8
|
|
1119
|
+
|
|
1120
|
+
|
|
1121
|
+
def int2bytes(n: int, byteorder='big', signed=False) -> bytes:
|
|
1122
|
+
"""Конвертировать `int` в `bytes` минимального размера"""
|
|
1123
|
+
f = int_size_signed if signed else int_size_unsigned
|
|
1124
|
+
return n.to_bytes(f(n), byteorder=byteorder, signed=signed)
|
|
1125
|
+
|
|
1126
|
+
|
|
1127
|
+
class SyncSignal(list):
|
|
1128
|
+
def __init__(self, ignore_errors=False):
|
|
1129
|
+
super().__init__()
|
|
1130
|
+
self.ignore_errors = bool(ignore_errors)
|
|
1131
|
+
|
|
1132
|
+
def send(self, *args, **kw):
|
|
1133
|
+
"""Отправить сигнал всем подписчикам"""
|
|
1134
|
+
e = not self.ignore_errors
|
|
1135
|
+
for func in list(self):
|
|
1136
|
+
try:
|
|
1137
|
+
func(*args, **kw)
|
|
1138
|
+
except Exception:
|
|
1139
|
+
if e:
|
|
1140
|
+
raise
|
|
1141
|
+
|
|
1142
|
+
|
|
1143
|
+
class AsyncSignal(SyncSignal):
|
|
1144
|
+
async def send(self, *args, **kw):
|
|
1145
|
+
e = not self.ignore_errors
|
|
1146
|
+
for func in list(self):
|
|
1147
|
+
try:
|
|
1148
|
+
await func(*args, **kw)
|
|
1149
|
+
except Exception:
|
|
1150
|
+
if e:
|
|
1151
|
+
raise
|
|
1152
|
+
|
|
1153
|
+
|
|
1154
|
+
class Filter:
|
|
1155
|
+
# Спасибо Pyrogram за идею
|
|
1156
|
+
def __call__(self, *a, **b):
|
|
1157
|
+
raise NotImplementedError
|
|
1158
|
+
|
|
1159
|
+
def __invert__(self):
|
|
1160
|
+
return _InvertedFilter(self)
|
|
1161
|
+
|
|
1162
|
+
def __and__(self, other):
|
|
1163
|
+
return _AndFilter(self, other)
|
|
1164
|
+
|
|
1165
|
+
def __or__(self, other):
|
|
1166
|
+
return _OrFilter(self, other)
|
|
1167
|
+
|
|
1168
|
+
|
|
1169
|
+
class _InvertedFilter(Filter):
|
|
1170
|
+
def __init__(self, f1: Filter):
|
|
1171
|
+
self.f1 = f1
|
|
1172
|
+
|
|
1173
|
+
def __call__(self, *a, **b):
|
|
1174
|
+
return not self.f1(*a, **b)
|
|
1175
|
+
|
|
1176
|
+
|
|
1177
|
+
class _AndFilter(Filter):
|
|
1178
|
+
def __init__(self, f1: Filter, f2: Filter):
|
|
1179
|
+
self.f1 = f1
|
|
1180
|
+
self.f2 = f2
|
|
1181
|
+
|
|
1182
|
+
def __call__(self, *a, **b):
|
|
1183
|
+
return self.f1(*a, **b) and self.f2(*a, **b)
|
|
1184
|
+
|
|
1185
|
+
|
|
1186
|
+
class _OrFilter(_AndFilter):
|
|
1187
|
+
def __call__(self, *a, **b):
|
|
1188
|
+
return self.f1(*a, **b) or self.f2(*a, **b)
|
|
1189
|
+
|
|
1190
|
+
|
|
1191
|
+
def create_filter(func: Callable, name: str = None, **d) -> Filter:
|
|
1192
|
+
d["__call__"] = func
|
|
1193
|
+
return type(name or func.__name__ or "CustomFilter", (Filter,), d)()
|
|
1194
|
+
|
|
1195
|
+
|
|
1196
|
+
filter_all = create_filter(return_True)
|
|
1197
|
+
filter_none = create_filter(return_False)
|
|
1198
|
+
|
|
1199
|
+
|
|
1200
|
+
class FilterGetAttr(Filter):
|
|
1201
|
+
def __init__(self, name: str):
|
|
1202
|
+
self.name = name
|
|
1203
|
+
|
|
1204
|
+
def __call__(self, obj):
|
|
1205
|
+
return getattr(obj, self.name, False)
|
|
1206
|
+
|
|
1207
|
+
|
|
1208
|
+
class FilterGetItem(FilterGetAttr):
|
|
1209
|
+
def __call__(self, obj):
|
|
1210
|
+
return obj[self.name]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/api/russian_trusted_root_ca_pem.crt
RENAMED
|
File without changes
|
{mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/api/russian_trusted_sub_ca_pem.crt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/ex/aiohttp_ex/web/__main__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/gui_scripts/ms2_hash_gen.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|