780 lines
24 KiB
Python
780 lines
24 KiB
Python
"""Semantic model for libksse command 1114139 / EngineProxy.sted.
|
|
|
|
This is not a full native allocator port. It captures the recovered behavior
|
|
that matters for the EGID/cache_m path:
|
|
|
|
* the decoded string table used by FUN_00142bf4;
|
|
* sentinel file names written by FUN_00140ea8 and read by FUN_00142bf4;
|
|
* suffix recovery polarity: existing path selects the candidate character;
|
|
* product-keyed JSON returned by FUN_00143d98.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import hashlib
|
|
import time
|
|
from dataclasses import dataclass
|
|
from typing import Callable, Iterable
|
|
|
|
from .ksse_crc import ksse_crc32
|
|
|
|
|
|
KSSE_DFP_TABLE_RAW = (
|
|
"0123456789ABCDEF@/.Android_@KUAISHOU@a21c40ada1eb475b@"
|
|
"DFP@/sdcard/Android@NEBULA"
|
|
)
|
|
|
|
KSSE_DOCUMENTS_DIR = "/sdcard/Documents"
|
|
STED_CACHE_FILE_TOKEN = "LnNrdmVj"
|
|
STED_CACHE_FILE_NAME = ".skvec"
|
|
STED_SHARED_PREF_KEY = "kwtk_n"
|
|
DFP_SUFFIX_LEN = 0x3D
|
|
|
|
|
|
class KsseSentinelMissing(ValueError):
|
|
"""Raised when the sentinel set cannot recover a full DFP suffix."""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class KsseDfpStringTable:
|
|
alphabet: str
|
|
android_hidden_dir_suffix: str
|
|
default_product: str
|
|
static_salt: str
|
|
dfp_prefix: str
|
|
sdcard_android_dir: str
|
|
active_product: str
|
|
|
|
@classmethod
|
|
def parse(cls, raw: str = KSSE_DFP_TABLE_RAW) -> "KsseDfpStringTable":
|
|
parts = raw.split("@")
|
|
if len(parts) != 7:
|
|
raise ValueError(f"unexpected libksse DFP table entry count: {len(parts)}")
|
|
return cls(
|
|
alphabet=parts[0],
|
|
android_hidden_dir_suffix=parts[1],
|
|
default_product=parts[2],
|
|
static_salt=parts[3],
|
|
dfp_prefix=parts[4],
|
|
sdcard_android_dir=parts[5],
|
|
active_product=parts[6],
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class StedJsonInsertionPlan:
|
|
"""One product-keyed JSON insertion attempt in ``FUN_00143d98``."""
|
|
|
|
output_key: str
|
|
stage: str
|
|
candidate_paths: list[str]
|
|
guard: str
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class StedPersistenceArtifacts:
|
|
"""Java/native EGID persistence artifacts after ``rq0.d.e(cache_e, cache_m)``."""
|
|
|
|
cache_json: str
|
|
in_memory_cache: dict[str, str]
|
|
shared_preferences: dict[str, str]
|
|
app_private_files: dict[str, str]
|
|
product_marker: str
|
|
native_base_path: str
|
|
native_sentinel_paths: list[str]
|
|
native_readback_json: str
|
|
|
|
|
|
def normalize_product_marker(marker: str | None, fallback: str = "NEBULA") -> str:
|
|
"""Convert native marker like ``0NEBULA`` / ``1KWE_N`` to product key."""
|
|
|
|
if not marker:
|
|
return fallback
|
|
marker = str(marker)
|
|
if marker[:1] in {"0", "1"}:
|
|
marker = marker[1:]
|
|
return marker or fallback
|
|
|
|
|
|
def ksse_md5_hex16(data: str | bytes) -> str:
|
|
"""Return native ``FUN_001575d0`` + ``FUN_00151204(..., 8)`` output.
|
|
|
|
``FUN_00151204`` uses the lowercase nibble table at ``DAT_0015cd1f``
|
|
(`0123456789abcdef...`), and top-level callers pass only the first
|
|
8 digest bytes, so the Python equivalent is the first 16 lowercase
|
|
hexadecimal characters of MD5.
|
|
"""
|
|
|
|
if isinstance(data, str):
|
|
data = data.encode("utf-8")
|
|
return hashlib.md5(data).hexdigest()[:16]
|
|
|
|
|
|
def engine_sted_product_marker(product: str = "NEBULA", writable_external_storage: bool = False) -> str:
|
|
"""Return EngineProxy.sted's native marker argument.
|
|
|
|
Java builds this as ``("1" if z else "0") + productName`` before calling
|
|
``Watermelon.jniCommand(1114139, "", str, marker)``. On targetSdk >= 30
|
|
the rq0.d.j() caller passes ``z=false``; legacy external-storage paths can
|
|
pass ``z=true`` when READ/WRITE external storage permission is available.
|
|
"""
|
|
|
|
product = normalize_product_marker(product)
|
|
return ("1" if writable_external_storage else "0") + product
|
|
|
|
|
|
def build_sted_cache_json(cache_e: str, cache_m: str, c_time_ms: int) -> str:
|
|
"""Build the compact JSONObject written by ``rq0.d.e``.
|
|
|
|
Native Java insertion order is ``c_time`` -> ``cache_e`` -> ``cache_m``.
|
|
The same text is written to SharedPreferences ``kwtk_n`` and app-private
|
|
files ``.skvec``.
|
|
"""
|
|
|
|
return json.dumps(
|
|
{
|
|
"c_time": int(c_time_ms),
|
|
"cache_e": str(cache_e),
|
|
"cache_m": str(cache_m),
|
|
},
|
|
ensure_ascii=False,
|
|
separators=(",", ":"),
|
|
)
|
|
|
|
|
|
def hidden_cache_path(base_path: str, suffix: str, table: KsseDfpStringTable | None = None) -> str:
|
|
"""Return ``base + '/.Android_' + suffix`` as constructed by native code."""
|
|
|
|
table = table or KsseDfpStringTable.parse()
|
|
return f"{str(base_path).rstrip('/')}{table.android_hidden_dir_suffix}{suffix}"
|
|
|
|
|
|
def sted_external_candidate_paths(
|
|
product_marker: str | None,
|
|
*,
|
|
table: KsseDfpStringTable | None = None,
|
|
documents_dir: str = KSSE_DOCUMENTS_DIR,
|
|
) -> list[str]:
|
|
"""First ``FUN_00143d98`` candidate pair: Documents then Android.
|
|
|
|
Native initializes the primary base from the decoded ``/sdcard/Documents``
|
|
constant and the fallback from table slot ``0x78`` (``/sdcard/Android``).
|
|
For ``KUAISHOU`` it appends the static salt; every other product key uses
|
|
``md5(product_key)[:16]``.
|
|
"""
|
|
|
|
table = table or KsseDfpStringTable.parse()
|
|
product = normalize_product_marker(product_marker, table.active_product)
|
|
suffix = table.static_salt if product == table.default_product else ksse_md5_hex16(product)
|
|
return [
|
|
hidden_cache_path(documents_dir, suffix, table),
|
|
hidden_cache_path(table.sdcard_android_dir, suffix, table),
|
|
]
|
|
|
|
|
|
def sted_product_salt_candidate_paths(
|
|
*,
|
|
table: KsseDfpStringTable | None = None,
|
|
documents_dir: str = KSSE_DOCUMENTS_DIR,
|
|
) -> list[str]:
|
|
"""Candidate pair using the static table salt ``a21c40ada1eb475b``."""
|
|
|
|
table = table or KsseDfpStringTable.parse()
|
|
return [
|
|
hidden_cache_path(documents_dir, table.static_salt, table),
|
|
hidden_cache_path(table.sdcard_android_dir, table.static_salt, table),
|
|
]
|
|
|
|
|
|
def sted_hidden_md5_candidate_paths(
|
|
*,
|
|
table: KsseDfpStringTable | None = None,
|
|
documents_dir: str = KSSE_DOCUMENTS_DIR,
|
|
active_product: str | None = None,
|
|
) -> list[str]:
|
|
"""Candidate pair using ``md5(table[0x90])[:16]``.
|
|
|
|
Static flow hashes the active table product (currently ``NEBULA``), not
|
|
the dynamic marker passed in ``param_5``.
|
|
"""
|
|
|
|
table = table or KsseDfpStringTable.parse()
|
|
product = active_product or table.active_product
|
|
suffix = ksse_md5_hex16(product)
|
|
return [
|
|
hidden_cache_path(documents_dir, suffix, table),
|
|
hidden_cache_path(table.sdcard_android_dir, suffix, table),
|
|
]
|
|
|
|
|
|
def sted_candidate_path_families(
|
|
product_marker: str | None,
|
|
*,
|
|
table: KsseDfpStringTable | None = None,
|
|
documents_dir: str = KSSE_DOCUMENTS_DIR,
|
|
) -> dict[str, list[str]]:
|
|
"""Return the ordered path families currently recovered from ``sted``."""
|
|
|
|
table = table or KsseDfpStringTable.parse()
|
|
return {
|
|
"external": sted_external_candidate_paths(
|
|
product_marker,
|
|
table=table,
|
|
documents_dir=documents_dir,
|
|
),
|
|
"product_salt": sted_product_salt_candidate_paths(
|
|
table=table,
|
|
documents_dir=documents_dir,
|
|
),
|
|
"hidden_md5": sted_hidden_md5_candidate_paths(
|
|
table=table,
|
|
documents_dir=documents_dir,
|
|
),
|
|
}
|
|
|
|
|
|
def sted_json_insertion_plan(
|
|
product_marker: str | None,
|
|
*,
|
|
table: KsseDfpStringTable | None = None,
|
|
documents_dir: str = KSSE_DOCUMENTS_DIR,
|
|
) -> list[StedJsonInsertionPlan]:
|
|
"""Return native JSON insertion attempts for ``FUN_00143d98``.
|
|
|
|
Each attempt still depends on successful suffix recovery. The native code
|
|
checks the output string length before prepending ``DFP`` and inserting the
|
|
member into the JSON object.
|
|
"""
|
|
|
|
table = table or KsseDfpStringTable.parse()
|
|
product = normalize_product_marker(product_marker, table.active_product)
|
|
plans = [
|
|
StedJsonInsertionPlan(
|
|
output_key=product,
|
|
stage="external_current",
|
|
candidate_paths=sted_external_candidate_paths(
|
|
product,
|
|
table=table,
|
|
documents_dir=documents_dir,
|
|
),
|
|
guard="always attempted first; insert only if recovered suffix is non-empty",
|
|
)
|
|
]
|
|
|
|
if product == table.active_product:
|
|
plans.append(
|
|
StedJsonInsertionPlan(
|
|
output_key=table.default_product,
|
|
stage="product_salt_default",
|
|
candidate_paths=sted_product_salt_candidate_paths(
|
|
table=table,
|
|
documents_dir=documents_dir,
|
|
),
|
|
guard="bVar6 == true; product matches table[0x90] active product",
|
|
)
|
|
)
|
|
elif product == table.default_product:
|
|
plans.append(
|
|
StedJsonInsertionPlan(
|
|
output_key=table.active_product,
|
|
stage="hidden_md5_active",
|
|
candidate_paths=sted_hidden_md5_candidate_paths(
|
|
table=table,
|
|
documents_dir=documents_dir,
|
|
),
|
|
guard="bVar1 == true; product matches table[0x30] default product",
|
|
)
|
|
)
|
|
else:
|
|
plans.extend(
|
|
[
|
|
StedJsonInsertionPlan(
|
|
output_key=table.default_product,
|
|
stage="product_salt_default",
|
|
candidate_paths=sted_product_salt_candidate_paths(
|
|
table=table,
|
|
documents_dir=documents_dir,
|
|
),
|
|
guard="!bVar1 && !bVar6; product is neither default nor active",
|
|
),
|
|
StedJsonInsertionPlan(
|
|
output_key=table.active_product,
|
|
stage="hidden_md5_active",
|
|
candidate_paths=sted_hidden_md5_candidate_paths(
|
|
table=table,
|
|
documents_dir=documents_dir,
|
|
),
|
|
guard="!bVar1 && !bVar6; product is neither default nor active",
|
|
),
|
|
]
|
|
)
|
|
|
|
return plans
|
|
|
|
|
|
def join_sentinel_base(base_path: str) -> str:
|
|
"""Return the native sentinel prefix equivalent to ``base + '/.'``."""
|
|
|
|
base = str(base_path).rstrip("/")
|
|
return f"{base}/."
|
|
|
|
|
|
def sentinel_half(index: int, char: str) -> str:
|
|
"""Return the half marker value used by FUN_00140ea8.
|
|
|
|
Native compares the byte with ASCII ``'8'``:
|
|
``0`` covers 0..7 and ``1`` covers 8..F for the uppercase hex alphabet.
|
|
"""
|
|
|
|
if len(char) != 1:
|
|
raise ValueError("char must be one byte/character")
|
|
return "0" if ord(char) < ord("8") else "1"
|
|
|
|
|
|
def sentinel_half_path(base_path: str, index: int, half: str | int) -> str:
|
|
"""Path for ``.<index>@<half>``."""
|
|
|
|
if index < 1:
|
|
raise ValueError("native sentinel indices are one-based")
|
|
half_text = str(half)
|
|
if half_text not in {"0", "1"}:
|
|
raise ValueError("half must be 0 or 1")
|
|
return f"{join_sentinel_base(base_path)}{index}@{half_text}"
|
|
|
|
|
|
def sentinel_char_path(base_path: str, index: int, char: str) -> str:
|
|
"""Path for ``.<index>_<actual_hex_char>``."""
|
|
|
|
if index < 1:
|
|
raise ValueError("native sentinel indices are one-based")
|
|
if len(char) != 1:
|
|
raise ValueError("char must be one byte/character")
|
|
return f"{join_sentinel_base(base_path)}{index}_{char}"
|
|
|
|
|
|
def sentinel_paths_for_suffix(base_path: str, suffix: str) -> list[str]:
|
|
"""Return all sentinel paths FUN_00140ea8 would materialize."""
|
|
|
|
paths: list[str] = []
|
|
for index, char in enumerate(suffix, start=1):
|
|
paths.append(sentinel_half_path(base_path, index, sentinel_half(index, char)))
|
|
paths.append(sentinel_char_path(base_path, index, char))
|
|
return paths
|
|
|
|
|
|
def recover_suffix_from_sentinels(
|
|
base_path: str,
|
|
exists: Callable[[str], bool],
|
|
*,
|
|
alphabet: str | None = None,
|
|
length: int = DFP_SUFFIX_LEN,
|
|
) -> str:
|
|
"""Recover the 61-byte DFP suffix using FUN_00142bf4's probe order."""
|
|
|
|
table = KsseDfpStringTable.parse()
|
|
alphabet = alphabet or table.alphabet
|
|
if len(alphabet) < 16:
|
|
raise ValueError("alphabet must contain at least 16 characters")
|
|
|
|
recovered: list[str] = []
|
|
for index in range(1, length + 1):
|
|
if exists(sentinel_half_path(base_path, index, "0")):
|
|
candidates = alphabet[:8]
|
|
elif exists(sentinel_half_path(base_path, index, "1")):
|
|
candidates = alphabet[8:16]
|
|
else:
|
|
candidates = alphabet[:16]
|
|
|
|
for char in candidates:
|
|
if exists(sentinel_char_path(base_path, index, char)):
|
|
recovered.append(char)
|
|
break
|
|
else:
|
|
raise KsseSentinelMissing(
|
|
f"missing sentinel for suffix index {index} under {base_path!r}"
|
|
)
|
|
|
|
return "".join(recovered)
|
|
|
|
|
|
def ksse_suffix_crc_material(
|
|
suffix: str,
|
|
*,
|
|
model: str = "",
|
|
table_raw: str = KSSE_DFP_TABLE_RAW,
|
|
) -> bytes:
|
|
"""Material that FUN_00142bf4 feeds to FUN_001567b8 / CRC32.
|
|
|
|
Static evidence shows the native code appends the raw table, then the
|
|
recovered suffix after in-place reversal, then ``ro.product.model``.
|
|
"""
|
|
|
|
return (table_raw + suffix[::-1] + model).encode("utf-8")
|
|
|
|
|
|
def ksse_suffix_crc32(
|
|
suffix: str,
|
|
*,
|
|
model: str = "",
|
|
table_raw: str = KSSE_DFP_TABLE_RAW,
|
|
) -> int:
|
|
return ksse_crc32(ksse_suffix_crc_material(suffix, model=model, table_raw=table_raw))
|
|
|
|
|
|
def dfp_suffix_from_value(
|
|
value: str,
|
|
*,
|
|
table: KsseDfpStringTable | None = None,
|
|
strict: bool = True,
|
|
) -> str:
|
|
"""Return the 61-byte suffix from ``DFP...`` or a raw suffix string."""
|
|
|
|
table = table or KsseDfpStringTable.parse()
|
|
text = str(value)
|
|
if text.startswith(table.dfp_prefix):
|
|
text = text[len(table.dfp_prefix) :]
|
|
if strict and len(text) != DFP_SUFFIX_LEN:
|
|
raise ValueError(f"DFP suffix must be {DFP_SUFFIX_LEN} characters, got {len(text)}")
|
|
return text
|
|
|
|
|
|
def ksse_dfp_suffix_crc_material(
|
|
suffix: str,
|
|
*,
|
|
model: str = "",
|
|
table_raw: str = KSSE_DFP_TABLE_RAW,
|
|
) -> bytes:
|
|
"""CRC material after native reverses recovered marker-order suffix back."""
|
|
|
|
if len(suffix) != DFP_SUFFIX_LEN:
|
|
raise ValueError(f"DFP suffix must be {DFP_SUFFIX_LEN} characters, got {len(suffix)}")
|
|
return (table_raw + suffix + model).encode("utf-8")
|
|
|
|
|
|
def ksse_dfp_suffix_crc32(
|
|
suffix: str,
|
|
*,
|
|
model: str = "",
|
|
table_raw: str = KSSE_DFP_TABLE_RAW,
|
|
) -> int:
|
|
return ksse_crc32(ksse_dfp_suffix_crc_material(suffix, model=model, table_raw=table_raw))
|
|
|
|
|
|
def sted_writer_marker_suffix(value: str, *, table: KsseDfpStringTable | None = None) -> str:
|
|
"""Suffix order written by ``FUN_00140ea8`` sentinel files.
|
|
|
|
The writer receives a Java-visible ``DFP`` value, strips the ``DFP`` prefix,
|
|
reverses the 61-byte suffix, then materializes sentinels for that reversed
|
|
byte order.
|
|
"""
|
|
|
|
return dfp_suffix_from_value(value, table=table)[::-1]
|
|
|
|
|
|
def sted_crc_marker_path(
|
|
base_path: str,
|
|
value: str,
|
|
*,
|
|
model: str = "",
|
|
table: KsseDfpStringTable | None = None,
|
|
table_raw: str = KSSE_DFP_TABLE_RAW,
|
|
) -> str:
|
|
"""Path for the CRC guard marker written after all byte sentinels."""
|
|
|
|
suffix = dfp_suffix_from_value(value, table=table)
|
|
crc = ksse_dfp_suffix_crc32(suffix, model=model, table_raw=table_raw)
|
|
return f"{join_sentinel_base(base_path)}{crc}"
|
|
|
|
|
|
def sted_writer_sentinel_paths(base_path: str, value: str) -> list[str]:
|
|
"""Per-byte sentinel files created by ``FUN_00140ea8`` for a DFP value."""
|
|
|
|
return sentinel_paths_for_suffix(base_path, sted_writer_marker_suffix(value))
|
|
|
|
|
|
def sted_writer_paths(
|
|
base_path: str,
|
|
value: str,
|
|
*,
|
|
model: str = "",
|
|
include_crc: bool = True,
|
|
) -> list[str]:
|
|
"""All deterministic marker paths created by ``FUN_00140ea8``.
|
|
|
|
File contents are just a small truth marker; path names carry the data.
|
|
"""
|
|
|
|
paths = sted_writer_sentinel_paths(base_path, value)
|
|
if include_crc:
|
|
paths.append(sted_crc_marker_path(base_path, value, model=model))
|
|
return paths
|
|
|
|
|
|
def build_sted_persistence_artifacts(
|
|
cache_e: str,
|
|
cache_m: str,
|
|
*,
|
|
c_time_ms: int | None = None,
|
|
product: str = "NEBULA",
|
|
writable_external_storage: bool = False,
|
|
model: str = "",
|
|
include_crc: bool = True,
|
|
table: KsseDfpStringTable | None = None,
|
|
documents_dir: str = KSSE_DOCUMENTS_DIR,
|
|
) -> StedPersistenceArtifacts:
|
|
"""Build deterministic Java/native persistence artifacts for an EGID.
|
|
|
|
This models the confirmed ``rq0.d.e(cache_e, cache_m)`` chain:
|
|
|
|
1. put ``c_time/cache_e/cache_m`` into the in-memory map;
|
|
2. write the same JSON to SharedPreferences key ``kwtk_n``;
|
|
3. write the same JSON to app-private file ``.skvec``;
|
|
4. call ``EngineProxy.sted(cache_e, z)`` to materialize native sentinel
|
|
files for command ``1114139``.
|
|
|
|
It deliberately does not try to reproduce the optional external
|
|
Java-serialized ``LinkedHashMap`` file from ``rq0.d.j`` because that path is
|
|
encrypted through ``uq0.o`` and is not needed for the native STED readback.
|
|
"""
|
|
|
|
table = table or KsseDfpStringTable.parse()
|
|
dfp_suffix_from_value(cache_e, table=table)
|
|
if c_time_ms is None:
|
|
c_time_ms = int(time.time() * 1000)
|
|
cache_json = build_sted_cache_json(cache_e, cache_m, c_time_ms)
|
|
product_marker = engine_sted_product_marker(
|
|
product,
|
|
writable_external_storage=writable_external_storage,
|
|
)
|
|
native_base_path = sted_external_candidate_paths(
|
|
product_marker,
|
|
table=table,
|
|
documents_dir=documents_dir,
|
|
)[0]
|
|
native_sentinel_paths = sted_writer_paths(
|
|
native_base_path,
|
|
cache_e,
|
|
model=model,
|
|
include_crc=include_crc,
|
|
)
|
|
existing = set(native_sentinel_paths)
|
|
native_readback_json = recover_sted_result_json_from_sentinels(
|
|
product_marker,
|
|
existing.__contains__,
|
|
model=model,
|
|
require_crc=include_crc,
|
|
table=table,
|
|
documents_dir=documents_dir,
|
|
)
|
|
return StedPersistenceArtifacts(
|
|
cache_json=cache_json,
|
|
in_memory_cache={
|
|
"c_time": str(int(c_time_ms)),
|
|
"cache_e": str(cache_e),
|
|
"cache_m": str(cache_m),
|
|
},
|
|
shared_preferences={STED_SHARED_PREF_KEY: cache_json},
|
|
app_private_files={STED_CACHE_FILE_NAME: cache_json},
|
|
product_marker=product_marker,
|
|
native_base_path=native_base_path,
|
|
native_sentinel_paths=native_sentinel_paths,
|
|
native_readback_json=native_readback_json,
|
|
)
|
|
|
|
|
|
def recover_dfp_suffix_from_sted_sentinels(
|
|
base_path: str,
|
|
exists: Callable[[str], bool],
|
|
*,
|
|
model: str = "",
|
|
require_crc: bool = False,
|
|
) -> str:
|
|
"""Recover the Java-visible DFP suffix from writer-created sentinels."""
|
|
|
|
marker_order = recover_suffix_from_sentinels(base_path, exists)
|
|
suffix = marker_order[::-1]
|
|
if require_crc and not exists(sted_crc_marker_path(base_path, suffix, model=model)):
|
|
raise KsseSentinelMissing(f"missing CRC marker for recovered suffix under {base_path!r}")
|
|
return suffix
|
|
|
|
|
|
def recover_dfp_value_from_sted_sentinels(
|
|
base_path: str,
|
|
exists: Callable[[str], bool],
|
|
*,
|
|
model: str = "",
|
|
require_crc: bool = False,
|
|
table: KsseDfpStringTable | None = None,
|
|
) -> str:
|
|
"""Recover ``DFP`` + suffix from writer-created sentinels."""
|
|
|
|
table = table or KsseDfpStringTable.parse()
|
|
suffix = recover_dfp_suffix_from_sted_sentinels(
|
|
base_path,
|
|
exists,
|
|
model=model,
|
|
require_crc=require_crc,
|
|
)
|
|
return table.dfp_prefix + suffix
|
|
|
|
|
|
def recover_first_dfp_value_from_candidate_paths(
|
|
candidate_paths: Iterable[str],
|
|
exists: Callable[[str], bool],
|
|
*,
|
|
model: str = "",
|
|
require_crc: bool = False,
|
|
table: KsseDfpStringTable | None = None,
|
|
) -> str | None:
|
|
"""Try native primary/fallback path order and return the first DFP value."""
|
|
|
|
table = table or KsseDfpStringTable.parse()
|
|
for base_path in candidate_paths:
|
|
try:
|
|
return recover_dfp_value_from_sted_sentinels(
|
|
base_path,
|
|
exists,
|
|
model=model,
|
|
require_crc=require_crc,
|
|
table=table,
|
|
)
|
|
except KsseSentinelMissing:
|
|
continue
|
|
return None
|
|
|
|
|
|
def recover_sted_values_from_sentinels(
|
|
product_marker: str | None,
|
|
exists: Callable[[str], bool],
|
|
*,
|
|
model: str = "",
|
|
require_crc: bool = False,
|
|
table: KsseDfpStringTable | None = None,
|
|
documents_dir: str = KSSE_DOCUMENTS_DIR,
|
|
) -> dict[str, str]:
|
|
"""Recover ``FUN_00143d98`` product-keyed values from sentinel files.
|
|
|
|
This models the Java-visible ``EngineProxy.sted(null,z)`` read path:
|
|
every planned JSON member is attempted in native insertion order, each
|
|
member uses its own primary/fallback path pair, and missing sentinel sets
|
|
simply skip that JSON member.
|
|
"""
|
|
|
|
table = table or KsseDfpStringTable.parse()
|
|
values: dict[str, str] = {}
|
|
for plan in sted_json_insertion_plan(
|
|
product_marker,
|
|
table=table,
|
|
documents_dir=documents_dir,
|
|
):
|
|
value = recover_first_dfp_value_from_candidate_paths(
|
|
plan.candidate_paths,
|
|
exists,
|
|
model=model,
|
|
require_crc=require_crc,
|
|
table=table,
|
|
)
|
|
if value:
|
|
values[plan.output_key] = value
|
|
return values
|
|
|
|
|
|
def recover_sted_result_json_from_sentinels(
|
|
product_marker: str | None,
|
|
exists: Callable[[str], bool],
|
|
*,
|
|
model: str = "",
|
|
require_crc: bool = False,
|
|
table: KsseDfpStringTable | None = None,
|
|
documents_dir: str = KSSE_DOCUMENTS_DIR,
|
|
) -> str:
|
|
"""Return compact product-keyed JSON recovered from native sentinels."""
|
|
|
|
return build_sted_result_json(
|
|
recover_sted_values_from_sentinels(
|
|
product_marker,
|
|
exists,
|
|
model=model,
|
|
require_crc=require_crc,
|
|
table=table,
|
|
documents_dir=documents_dir,
|
|
)
|
|
)
|
|
|
|
|
|
def build_sted_result_json(values: dict[str, str]) -> str:
|
|
"""Serialize product-keyed DFP values like FUN_00154a0c compact mode."""
|
|
|
|
return json.dumps(values, ensure_ascii=False, separators=(",", ":"))
|
|
|
|
|
|
def parse_sted_result_json(data: str) -> dict[str, str]:
|
|
parsed = json.loads(data)
|
|
if not isinstance(parsed, dict):
|
|
raise ValueError("sted result must be a JSON object")
|
|
return {str(key): str(value) for key, value in parsed.items()}
|
|
|
|
|
|
def select_sted_product_value(data: str | dict[str, str], marker: str | None) -> str:
|
|
"""Select current product's DFP value from FUN_00143d98-style JSON."""
|
|
|
|
values = parse_sted_result_json(data) if isinstance(data, str) else data
|
|
product = normalize_product_marker(marker)
|
|
if product in values:
|
|
return values[product]
|
|
table = KsseDfpStringTable.parse()
|
|
if table.default_product in values:
|
|
return values[table.default_product]
|
|
if values:
|
|
return next(iter(values.values()))
|
|
raise KeyError("empty sted result")
|
|
|
|
|
|
__all__ = [
|
|
"DFP_SUFFIX_LEN",
|
|
"KSSE_DOCUMENTS_DIR",
|
|
"KSSE_DFP_TABLE_RAW",
|
|
"STED_CACHE_FILE_NAME",
|
|
"STED_CACHE_FILE_TOKEN",
|
|
"STED_SHARED_PREF_KEY",
|
|
"KsseDfpStringTable",
|
|
"KsseSentinelMissing",
|
|
"StedJsonInsertionPlan",
|
|
"StedPersistenceArtifacts",
|
|
"build_sted_cache_json",
|
|
"build_sted_persistence_artifacts",
|
|
"build_sted_result_json",
|
|
"dfp_suffix_from_value",
|
|
"engine_sted_product_marker",
|
|
"hidden_cache_path",
|
|
"join_sentinel_base",
|
|
"ksse_dfp_suffix_crc32",
|
|
"ksse_dfp_suffix_crc_material",
|
|
"ksse_md5_hex16",
|
|
"ksse_suffix_crc32",
|
|
"ksse_suffix_crc_material",
|
|
"normalize_product_marker",
|
|
"parse_sted_result_json",
|
|
"recover_first_dfp_value_from_candidate_paths",
|
|
"recover_dfp_value_from_sted_sentinels",
|
|
"recover_dfp_suffix_from_sted_sentinels",
|
|
"recover_suffix_from_sentinels",
|
|
"recover_sted_result_json_from_sentinels",
|
|
"recover_sted_values_from_sentinels",
|
|
"select_sted_product_value",
|
|
"sentinel_char_path",
|
|
"sentinel_half",
|
|
"sentinel_half_path",
|
|
"sentinel_paths_for_suffix",
|
|
"sted_candidate_path_families",
|
|
"sted_external_candidate_paths",
|
|
"sted_hidden_md5_candidate_paths",
|
|
"sted_json_insertion_plan",
|
|
"sted_product_salt_candidate_paths",
|
|
"sted_crc_marker_path",
|
|
"sted_writer_marker_suffix",
|
|
"sted_writer_paths",
|
|
"sted_writer_sentinel_paths",
|
|
]
|