Skip to content

Feat/xrap param - #108

Merged
Cloxl merged 8 commits into
masterfrom
feat/xrap-param
Jun 11, 2026
Merged

Feat/xrap param#108
Cloxl merged 8 commits into
masterfrom
feat/xrap-param

Conversation

@Cloxl

@Cloxl Cloxl commented Jun 11, 2026

Copy link
Copy Markdown
Owner

Summary by Sourcery

添加对生成 x-rap-param 请求头及相关分片/搜索工具的支持,更新加密配置,并通过精简示例和开发流程刷新文档。

New Features:

  • 提供用于搜索端点的 search_idsearch_request_id 生成辅助函数。
  • 提供用于生成 a1web_id Cookie 值的静态辅助函数。
  • 添加 x-rap-param 生成流水线,包括类 SM4 的纯 Python 密码算法、xxh32 哈希和分片工具,并将其集成到带有 xy-direction 和 x-mns 请求头的 sign_headers API 中。
  • 从公共模块中导出 xxh32、x_rap_param 辅助函数以及分片键工具。

Enhancements:

  • 更新加密配置 SDK 版本和签名模板,以与新的上游参数保持一致。
  • 简化并现代化 README 中的使用示例,文档化 x-rap-param 支持、新的辅助 API,以及开发工作流。

Tests:

  • 添加测试以验证 x-rap 密码分组加密、会话密钥封装以及 x-rap-param 数据包结构。
Original summary in English

Summary by Sourcery

Add support for generating x-rap-param headers and related sharding/search utilities, update crypto configuration, and refresh documentation with streamlined examples and development workflow.

New Features:

  • Expose helpers to generate search_id and search_request_id for search endpoints.
  • Expose static helpers to generate a1 and web_id cookie values.
  • Add x-rap-param generation pipeline, including pure-Python SM4-like cipher, xxh32 hash, and sharding utilities, and integrate it into sign_headers APIs with xy-direction and x-mns headers.
  • Expose xxh32 and x_rap_param helpers and sharding key utility from public modules.

Enhancements:

  • Update crypto configuration SDK versions and signature templates to align with newer upstream parameters.
  • Simplify and modernize README usage examples, documenting x-rap-param support, new helper APIs, and development workflow.

Tests:

  • Add tests validating x-rap cipher block encryption, session key wrapping, and x-rap-param packet shape.

@sourcery-ai

sourcery-ai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Reviewer's Guide

添加 x-rap-param 支持以及搜索/账号辅助工具,将它们接入签名客户端,并刷新文档/配置以匹配最新的 XHS Web 协议。

使用 x-rap-param 生成请求头的时序图

sequenceDiagram
    participant User
    participant Xhshow
    participant RandomGenerator
    participant Sharding as get_sharding_key
    participant XRAP as x_rap_param

    User->>Xhshow: sign_headers_post(uri, cookies, payload, x_rap=True, user_id)
    Xhshow->>Xhshow: sign_headers(method="POST", uri, cookies, payload, user_id, x_rap)
    Xhshow->>RandomGenerator: generate_x_t
    RandomGenerator-->>Xhshow: x_t
    Xhshow->>RandomGenerator: generate_b3_trace_id
    RandomGenerator-->>Xhshow: x_b3_traceid
    Xhshow->>RandomGenerator: generate_xray_trace_id
    RandomGenerator-->>Xhshow: x_xray_traceid
    Xhshow->>Sharding: get_sharding_key(user_id)
    Sharding-->>Xhshow: xy_direction
    alt x_rap is True
        Xhshow->>XRAP: x_rap_param(rap_api, request_data)
        XRAP-->>Xhshow: x_rap_param_value
    end
    Xhshow-->>User: headers {x-s, x-s-common, x-t, x-b3-traceid, x-xray-traceid, x-mns, xy-direction, x-rap-param?}
Loading

File-Level Changes

Change Details Files
在 Xhshow 上暴露新的搜索 ID 和账号 Cookie 辅助方法,并扩展请求头签名 API,使其支持 x-rap-param 和分片(sharding)。
  • 添加 get_search_id 和 get_search_request_id 方法,并委托给 RandomGenerator。
  • 暴露静态 generate_a1 和 generate_web_id 辅助方法,用于生成 Cookie。
  • 扩展 sign_headers / sign_headers_get / sign_headers_post,使其接受 user_id 和 x_rap 参数,并通过新工具接入 x-mns、xy-direction 和 x-rap-param 的生成。
src/xhshow/client.py
实现纯 Python 的 x-rap-param 基础原语以及相关的哈希/分片工具。
  • 为 RandomGenerator 添加 a1/web_id 生成和搜索 ID 的辅助方法;实现 base36 和随机 ASCII 工具。
  • 引入 xxh32 哈希实现,用于 x-rap,并从 core/utils 导出。
  • 添加 get_sharding_key(),通过对 user_id 进行类似 MurmurHash 的算法计算或随机回退值,生成 xy-direction。
  • 在 core.xrap 中实现 x_rap_param(),包括自定义的类似 SM4 的分组密码、TLV 体构造器、gzip 封装器和封包器,并从 core/init.py 和 utils/init.py 导出。
src/xhshow/utils/random_gen.py
src/xhshow/utils/hash.py
src/xhshow/utils/sharding.py
src/xhshow/core/xrap.py
src/xhshow/core/__init__.py
src/xhshow/utils/__init__.py
更新加密配置常量,以与更新后的 XHS SDK/Web 版本对齐,并添加 x-rap 协议版本。
  • 将 DATA_SDK_VERSION 及相关签名模版版本从 4.3.3/4.2.6 升级到 4.3.5,并将 x4 字段调整为 "object"。
  • 引入 XRAP_SDK_VERSION 常量,用于 x-rap-param 封包。
src/xhshow/config/config.py
为 x-rap-param 原语添加回归测试,并文档化新的高层 API 和相关概念。
  • 添加覆盖分组加密、会话密钥加密以及确定性 x_rap_param 包结构的测试。
  • 重写/精简 README 的使用说明部分,重点介绍快速上手、x-rap-param、搜索/账号辅助工具、单字段生成器、会话管理、工具、定制化以及开发流程,并包含使用 x_rap 和 xy-direction 的示例。
tests/test_xrap.py
README.md

Tips and commands

Interacting with Sourcery

  • 触发新的代码审查: 在 Pull Request 上评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的审查评论。
  • 从审查评论生成 GitHub Issue: 通过回复某条审查评论,要求 Sourcery 从该评论创建一个 issue。你也可以回复审查评论 @sourcery-ai issue 来从中创建 issue。
  • 生成 Pull Request 标题: 在 Pull Request 标题的任意位置写入 @sourcery-ai 以随时生成标题。你也可以在 Pull Request 上评论 @sourcery-ai title 来(重新)生成标题。
  • 生成 Pull Request 摘要: 在 Pull Request 内容的任意位置写入 @sourcery-ai summary,即可在指定位置生成 PR 摘要。你也可以在 Pull Request 上评论 @sourcery-ai summary 来在任意时间(重新)生成摘要。
  • 生成审查者指南: 在 Pull Request 上评论 @sourcery-ai guide,即可在任意时间(重新)生成审查者指南。
  • 解决所有 Sourcery 评论: 在 Pull Request 上评论 @sourcery-ai resolve 以标记所有 Sourcery 评论为已解决。如果你已经处理完所有评论且不想再看到它们,这会很有用。
  • 撤销所有 Sourcery 审查: 在 Pull Request 上评论 @sourcery-ai dismiss 以撤销所有现有的 Sourcery 审查。如果你想从头开始进行新的审查,这尤其有用——别忘了再评论 @sourcery-ai review 来触发新的审查!

Customizing Your Experience

访问你的 dashboard 以:

  • 启用或禁用审查功能,例如 Sourcery 自动生成的 Pull Request 摘要、审查者指南等。
  • 更改审查语言。
  • 添加、移除或编辑自定义审查指令。
  • 调整其他审查设置。

Getting Help

Original review guide in English

Reviewer's Guide

Adds x-rap-param support and search/account helper utilities, wires them into the signing client, and refreshes docs/config to match the latest XHS web protocol.

Sequence diagram for generating headers with x-rap-param

sequenceDiagram
    participant User
    participant Xhshow
    participant RandomGenerator
    participant Sharding as get_sharding_key
    participant XRAP as x_rap_param

    User->>Xhshow: sign_headers_post(uri, cookies, payload, x_rap=True, user_id)
    Xhshow->>Xhshow: sign_headers(method="POST", uri, cookies, payload, user_id, x_rap)
    Xhshow->>RandomGenerator: generate_x_t
    RandomGenerator-->>Xhshow: x_t
    Xhshow->>RandomGenerator: generate_b3_trace_id
    RandomGenerator-->>Xhshow: x_b3_traceid
    Xhshow->>RandomGenerator: generate_xray_trace_id
    RandomGenerator-->>Xhshow: x_xray_traceid
    Xhshow->>Sharding: get_sharding_key(user_id)
    Sharding-->>Xhshow: xy_direction
    alt x_rap is True
        Xhshow->>XRAP: x_rap_param(rap_api, request_data)
        XRAP-->>Xhshow: x_rap_param_value
    end
    Xhshow-->>User: headers {x-s, x-s-common, x-t, x-b3-traceid, x-xray-traceid, x-mns, xy-direction, x-rap-param?}
Loading

File-Level Changes

Change Details Files
Expose new helpers on Xhshow for search IDs and account cookies, and extend header signing APIs with x-rap-param and sharding support.
  • Add get_search_id and get_search_request_id methods delegating to RandomGenerator.
  • Expose static generate_a1 and generate_web_id helpers for cookie generation.
  • Extend sign_headers / sign_headers_get / sign_headers_post with user_id and x_rap parameters, wiring in x-mns, xy-direction, and x-rap-param generation using new utilities.
src/xhshow/client.py
Implement pure-Python primitives for x-rap-param generation and supporting hashing/sharding utilities.
  • Add RandomGenerator helpers for a1/web_id generation and search IDs; implement base36 and random ascii utilities.
  • Introduce xxh32 hash implementation used by x-rap and exported from core/utils.
  • Add get_sharding_key() to produce xy-direction via MurmurHash-like algorithm on user_id or a random fallback.
  • Implement x_rap_param() in core.xrap, including custom SM4-like block cipher, TLV body builder, gzip wrapper, and envelope packer, and export from core/init.py and utils/init.py.
src/xhshow/utils/random_gen.py
src/xhshow/utils/hash.py
src/xhshow/utils/sharding.py
src/xhshow/core/xrap.py
src/xhshow/core/__init__.py
src/xhshow/utils/__init__.py
Update cryptographic configuration constants to align with newer XHS SDK/web versions and add x-rap protocol version.
  • Bump DATA_SDK_VERSION and related signature template versions from 4.3.3/4.2.6 to 4.3.5 and adjust x4 field to "object".
  • Introduce XRAP_SDK_VERSION constant used by x-rap-param envelope.
src/xhshow/config/config.py
Add regression tests for x-rap-param primitives and document the new high-level API and concepts.
  • Add tests covering block cipher encryption, session key encryption, and deterministic x_rap_param packet shape.
  • Rewrite/condense README usage section to focus on quickstart, x-rap-param, search/account helpers, single-field generators, session management, tools, customization, and dev workflow, including examples using x_rap and xy-direction.
tests/test_xrap.py
README.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@Cloxl
Cloxl force-pushed the feat/xrap-param branch from 3319458 to 2707efb Compare June 11, 2026 10:14

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我发现了 4 个问题,并给出了一些整体性的反馈:

  • core/xrap.py 中存在很多硬编码的数字标签和 magic 常量(例如 TLV 标签 0x03E8+,以及 0x564/0x2C/0x218C 这样的值);建议把这些提取成具名常量或小型枚举,并加上简短注释,这样以后 RAP 协议变更时更容易追踪,也更不容易出错。
  • utils/hash.py 中的 xxh32 实现从一个新的 CryptoConfig() 实例中读取 _MASK_32,但其余部分只使用静态字面量;可以考虑直接用简单的 0xFFFFFFFF(或者复用现有的 32 位最大值常量)来代替,这样可以避免不必要的配置依赖,让该函数更加自包含。
面向 AI Agent 的提示
Please address the comments from this code review:

## Overall Comments
- In `core/xrap.py` there are many hard-coded numeric tags and magic constants (e.g. TLV tags 0x03E8+, values like 0x564/0x2C/0x218C); consider factoring these into named constants or small enums with brief comments so that future changes to the RAP protocol are easier to track and less error-prone.
- The `xxh32` implementation in `utils/hash.py` pulls `_MASK_32` from a new `CryptoConfig()` instance but otherwise uses only static literals; replacing this with a simple `0xFFFFFFFF` (or reusing the existing max-32-bit constant directly) would avoid an unnecessary config dependency and make the function more self-contained.

## Individual Comments

### Comment 1
<location path="src/xhshow/core/xrap.py" line_range="102-105" />
<code_context>
+    return int.from_bytes(buf[off : off + 4], "big")
+
+
+def _to_compact_json(data: Mapping[str, Any] | str | bytes | bytearray) -> str:
+    if isinstance(data, str):
+        return data
+    if isinstance(data, bytes | bytearray):
+        return bytes(data).decode("utf-8")
+    return json.dumps(data, ensure_ascii=False, separators=(",", ":"))
</code_context>
<issue_to_address>
**issue (bug_risk):** Use a tuple of types in isinstance() instead of a PEP 604 union

`isinstance(data, bytes | bytearray)` will raise a `TypeError` because `isinstance` requires a type or a tuple of types as its second argument, not a PEP 604 union. This will break bytes/bytearray handling at runtime. Please change it to `isinstance(data, (bytes, bytearray))`.
</issue_to_address>

### Comment 2
<location path="tests/test_xrap.py" line_range="16-25" />
<code_context>
+    assert _encrypt_session_key(b"wapilabkmyv4wl46").hex() == "fac980a920308a95885597eb7b8b150a00000010"
+
+
+def test_xrap_param_packet_shape():
+    value = x_rap_param(
+        "//edith.xiaohongshu.com/api/sns/web/v1/homefeed",
+        '{"a":1}',
+        aes_key="wapilabkmyv4wl46",
+        random_string="mdzz94",
+        inner_key="h9w3tl5em3w4t67c",
+        timestamp_ms=0x0000019EB07ACDB2,
+        gzip_mtime=0x6A291532,
+        body_encry_time=69,
+        body_rand32=0xF95AD1C7,
+        mask=0x65,
+    )
+    raw = base64.b64decode(value)
+    assert raw[:4].hex() == "07240106"
+    assert int.from_bytes(raw[4:8], "big") == 1
+    assert int.from_bytes(raw[8:12], "big") == 20
+    assert raw[36:42] == b"mdzz94"
</code_context>
<issue_to_address>
**suggestion (testing):** Strengthen x_rap_param test by asserting the full deterministic output when all randomness is fixed.

Since all random inputs in `test_xrap_param_packet_shape` are fixed, the resulting x-rap-param should be fully deterministic. Rather than only checking a few header fields and the salt, please also assert the complete base64 string or the full decoded payload to catch subtle changes in body structure, gzip flags, encryption, or header layout. If the full string is unwieldy, asserting the header plus a checksum (e.g., xxh32) of the remainder would still strengthen regression coverage.

Suggested implementation:

```python
def test_xrap_param_packet_shape():
    value = x_rap_param(
        "//edith.xiaohongshu.com/api/sns/web/v1/homefeed",
        '{"a":1}',
        aes_key="wapilabkmyv4wl46",
        random_string="mdzz94",
        inner_key="h9w3tl5em3w4t67c",
        timestamp_ms=0x0000019EB07ACDB2,
        gzip_mtime=0x6A291532,
        body_encry_time=69,
        body_rand32=0xF95AD1C7,
        mask=0x65,
    )
    raw = base64.b64decode(value)

    # header layout checks
    assert raw[:4].hex() == "07240106"
    assert int.from_bytes(raw[4:8], "big") == 1
    assert int.from_bytes(raw[8:12], "big") == 20
    assert raw[36:42] == b"mdzz94"

    # full-packet regression check: base64 output must be fully deterministic
    assert value == EXPECTED_XRAP_PARAM_B64

    # strengthen coverage further with a checksum of the encrypted body payload
    body = raw[12:]
    assert zlib.crc32(body) == EXPECTED_XRAP_PARAM_BODY_CRC32

```

To fully wire this change you should:
1. Import `zlib` at the top of `tests/test_xrap.py`:
   - `import zlib`
2. Define deterministic expectations near the top of the file (or next to the tests):
   - `EXPECTED_XRAP_PARAM_B64 = "<fill-with-observed-x_rap_param-output>"`  
   - `EXPECTED_XRAP_PARAM_BODY_CRC32 = <fill-with-int-crc32-of-raw[12:]>`
3. To obtain these values:
   - Temporarily print `value` and `zlib.crc32(base64.b64decode(value)[12:])` from the test, run it once, copy the printed values into the constants, and then remove the prints.
4. Once filled in, the test will assert both the complete base64-encoded x-rap-param and a checksum of the remaining payload, providing the strengthened regression coverage you requested.
</issue_to_address>

### Comment 3
<location path="tests/test_xrap.py" line_range="1-4" />
<code_context>
+import base64
+import gzip
+import json
</code_context>
<issue_to_address>
**suggestion (testing):** Add tests for other newly introduced public APIs (search IDs, a1/web_id, sharding key, and sign_headers x-rap/xy-direction).

This change adds several new public-facing behaviors (`generate_a1`, `generate_web_id`, `generate_search_id`, `generate_search_request_id`, `get_sharding_key`, and updated `sign_headers`/`sign_headers_get`/`sign_headers_post`) that currently lack test coverage. Please add tests to:
- Verify output length and determinism for `generate_a1` (52 chars) and `generate_web_id` (32-char hex from a known a1).
- Check `generate_search_id` / `generate_search_request_id` formats (base36, `random-timestamp` pattern) and monotonic behavior with fixed timestamps.
- Confirm `get_sharding_key` determinism, 1–100 range, and behavior when `user_id=None`.
- Ensure `sign_headers` (and GET/POST wrappers) set `x-mns="unload"`, compute `xy-direction` from `user_id`, and conditionally add `x-rap-param` when `x_rap=True`.
This will guard the new public APIs and prevent regressions in higher-level x-rap behavior.
</issue_to_address>

### Comment 4
<location path="src/xhshow/utils/sharding.py" line_range="19" />
<code_context>
+    return ((x << r) | (x >> (32 - r))) & 0xFFFFFFFF
+
+
+def get_sharding_key(user_id: str | None = None) -> int:
+    if user_id is None:
+        return random.randint(10, 100)
</code_context>
<issue_to_address>
**issue (complexity):** Consider refactoring the hashing logic into a reusable Murmur32 helper and replacing ctypes with pure bit-masked arithmetic to simplify get_sharding_key.

You can simplify the implementation without changing behaviour by:

1. Removing `ctypes` and using masking for 32‑bit arithmetic.
2. Extracting the Murmur32 logic into a helper.
3. Separating the “random when `user_id is None`” behaviour from the hashing.

For example:

```python
_C1 = 0xCC9E2D51
_C2 = 0x1B873593

def _imul(a: int, b: int) -> int:
    # 32‑bit multiply with wraparound
    return ((a & 0xFFFFFFFF) * (b & 0xFFFFFFFF)) & 0xFFFFFFFF

def _rotl32(x: int, r: int) -> int:
    x &= 0xFFFFFFFF
    return ((x << r) | (x >> (32 - r))) & 0xFFFFFFFF

def _murmur32(data: bytes, seed: int = 151488) -> int:
    length = len(data)
    r = seed

    for o in range(length // 4):
        i = 4 * o
        u = (
            data[i]
            | (data[i + 1] << 8)
            | (data[i + 2] << 16)
            | (data[i + 3] << 24)
        )
        u = _imul(u, _C1)
        u = _rotl32(u, 15)
        u = _imul(u, _C2)
        r ^= u
        r = _rotl32(r, 13)
        r = (_imul(r, 5) + 0xE6546B64) & 0xFFFFFFFF  # replaces ctypes.c_int32(...).value

    s = 4 * (length // 4)
    c = 0
    rem = length % 4
    if rem >= 3:
        c ^= data[s + 2] << 16
    if rem >= 2:
        c ^= data[s + 1] << 8
    if rem >= 1:
        c ^= data[s]
        c = _imul(c, _C1)
        c = _rotl32(c, 15)
        c = _imul(c, _C2)
        r ^= c

    r ^= length
    r &= 0xFFFFFFFF
    r ^= r >> 16
    r = _imul(r, 0x85EBCA6B) & 0xFFFFFFFF
    r ^= r >> 13
    r = _imul(r, 0xC2B2AE35) & 0xFFFFFFFF
    r ^= r >> 16
    return r
```

Then `get_sharding_key` becomes a thin wrapper with clearer behaviour:

```python
def get_sharding_key(user_id: str | None = None) -> int:
    if user_id is None:
        return random.randint(10, 100)
    h = _murmur32(user_id.encode("utf-8"))
    return (h % 100) + 1
```

This keeps the same hashing and random behaviour while:

- Dropping `ctypes` in favour of pure‑Python bit‑masking.
- Making the Murmur‑style hash reusable and easier to test.
- Reducing cognitive load in `get_sharding_key` by separating concerns.
</issue_to_address>

Sourcery 对开源项目免费——如果你觉得我们的评审有帮助,欢迎分享 ✨
帮我变得更有用!请在每条评论上点击 👍 或 👎,我会根据你的反馈改进评审质量。
Original comment in English

Hey - I've found 4 issues, and left some high level feedback:

  • In core/xrap.py there are many hard-coded numeric tags and magic constants (e.g. TLV tags 0x03E8+, values like 0x564/0x2C/0x218C); consider factoring these into named constants or small enums with brief comments so that future changes to the RAP protocol are easier to track and less error-prone.
  • The xxh32 implementation in utils/hash.py pulls _MASK_32 from a new CryptoConfig() instance but otherwise uses only static literals; replacing this with a simple 0xFFFFFFFF (or reusing the existing max-32-bit constant directly) would avoid an unnecessary config dependency and make the function more self-contained.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `core/xrap.py` there are many hard-coded numeric tags and magic constants (e.g. TLV tags 0x03E8+, values like 0x564/0x2C/0x218C); consider factoring these into named constants or small enums with brief comments so that future changes to the RAP protocol are easier to track and less error-prone.
- The `xxh32` implementation in `utils/hash.py` pulls `_MASK_32` from a new `CryptoConfig()` instance but otherwise uses only static literals; replacing this with a simple `0xFFFFFFFF` (or reusing the existing max-32-bit constant directly) would avoid an unnecessary config dependency and make the function more self-contained.

## Individual Comments

### Comment 1
<location path="src/xhshow/core/xrap.py" line_range="102-105" />
<code_context>
+    return int.from_bytes(buf[off : off + 4], "big")
+
+
+def _to_compact_json(data: Mapping[str, Any] | str | bytes | bytearray) -> str:
+    if isinstance(data, str):
+        return data
+    if isinstance(data, bytes | bytearray):
+        return bytes(data).decode("utf-8")
+    return json.dumps(data, ensure_ascii=False, separators=(",", ":"))
</code_context>
<issue_to_address>
**issue (bug_risk):** Use a tuple of types in isinstance() instead of a PEP 604 union

`isinstance(data, bytes | bytearray)` will raise a `TypeError` because `isinstance` requires a type or a tuple of types as its second argument, not a PEP 604 union. This will break bytes/bytearray handling at runtime. Please change it to `isinstance(data, (bytes, bytearray))`.
</issue_to_address>

### Comment 2
<location path="tests/test_xrap.py" line_range="16-25" />
<code_context>
+    assert _encrypt_session_key(b"wapilabkmyv4wl46").hex() == "fac980a920308a95885597eb7b8b150a00000010"
+
+
+def test_xrap_param_packet_shape():
+    value = x_rap_param(
+        "//edith.xiaohongshu.com/api/sns/web/v1/homefeed",
+        '{"a":1}',
+        aes_key="wapilabkmyv4wl46",
+        random_string="mdzz94",
+        inner_key="h9w3tl5em3w4t67c",
+        timestamp_ms=0x0000019EB07ACDB2,
+        gzip_mtime=0x6A291532,
+        body_encry_time=69,
+        body_rand32=0xF95AD1C7,
+        mask=0x65,
+    )
+    raw = base64.b64decode(value)
+    assert raw[:4].hex() == "07240106"
+    assert int.from_bytes(raw[4:8], "big") == 1
+    assert int.from_bytes(raw[8:12], "big") == 20
+    assert raw[36:42] == b"mdzz94"
</code_context>
<issue_to_address>
**suggestion (testing):** Strengthen x_rap_param test by asserting the full deterministic output when all randomness is fixed.

Since all random inputs in `test_xrap_param_packet_shape` are fixed, the resulting x-rap-param should be fully deterministic. Rather than only checking a few header fields and the salt, please also assert the complete base64 string or the full decoded payload to catch subtle changes in body structure, gzip flags, encryption, or header layout. If the full string is unwieldy, asserting the header plus a checksum (e.g., xxh32) of the remainder would still strengthen regression coverage.

Suggested implementation:

```python
def test_xrap_param_packet_shape():
    value = x_rap_param(
        "//edith.xiaohongshu.com/api/sns/web/v1/homefeed",
        '{"a":1}',
        aes_key="wapilabkmyv4wl46",
        random_string="mdzz94",
        inner_key="h9w3tl5em3w4t67c",
        timestamp_ms=0x0000019EB07ACDB2,
        gzip_mtime=0x6A291532,
        body_encry_time=69,
        body_rand32=0xF95AD1C7,
        mask=0x65,
    )
    raw = base64.b64decode(value)

    # header layout checks
    assert raw[:4].hex() == "07240106"
    assert int.from_bytes(raw[4:8], "big") == 1
    assert int.from_bytes(raw[8:12], "big") == 20
    assert raw[36:42] == b"mdzz94"

    # full-packet regression check: base64 output must be fully deterministic
    assert value == EXPECTED_XRAP_PARAM_B64

    # strengthen coverage further with a checksum of the encrypted body payload
    body = raw[12:]
    assert zlib.crc32(body) == EXPECTED_XRAP_PARAM_BODY_CRC32

```

To fully wire this change you should:
1. Import `zlib` at the top of `tests/test_xrap.py`:
   - `import zlib`
2. Define deterministic expectations near the top of the file (or next to the tests):
   - `EXPECTED_XRAP_PARAM_B64 = "<fill-with-observed-x_rap_param-output>"`  
   - `EXPECTED_XRAP_PARAM_BODY_CRC32 = <fill-with-int-crc32-of-raw[12:]>`
3. To obtain these values:
   - Temporarily print `value` and `zlib.crc32(base64.b64decode(value)[12:])` from the test, run it once, copy the printed values into the constants, and then remove the prints.
4. Once filled in, the test will assert both the complete base64-encoded x-rap-param and a checksum of the remaining payload, providing the strengthened regression coverage you requested.
</issue_to_address>

### Comment 3
<location path="tests/test_xrap.py" line_range="1-4" />
<code_context>
+import base64
+import gzip
+import json
</code_context>
<issue_to_address>
**suggestion (testing):** Add tests for other newly introduced public APIs (search IDs, a1/web_id, sharding key, and sign_headers x-rap/xy-direction).

This change adds several new public-facing behaviors (`generate_a1`, `generate_web_id`, `generate_search_id`, `generate_search_request_id`, `get_sharding_key`, and updated `sign_headers`/`sign_headers_get`/`sign_headers_post`) that currently lack test coverage. Please add tests to:
- Verify output length and determinism for `generate_a1` (52 chars) and `generate_web_id` (32-char hex from a known a1).
- Check `generate_search_id` / `generate_search_request_id` formats (base36, `random-timestamp` pattern) and monotonic behavior with fixed timestamps.
- Confirm `get_sharding_key` determinism, 1–100 range, and behavior when `user_id=None`.
- Ensure `sign_headers` (and GET/POST wrappers) set `x-mns="unload"`, compute `xy-direction` from `user_id`, and conditionally add `x-rap-param` when `x_rap=True`.
This will guard the new public APIs and prevent regressions in higher-level x-rap behavior.
</issue_to_address>

### Comment 4
<location path="src/xhshow/utils/sharding.py" line_range="19" />
<code_context>
+    return ((x << r) | (x >> (32 - r))) & 0xFFFFFFFF
+
+
+def get_sharding_key(user_id: str | None = None) -> int:
+    if user_id is None:
+        return random.randint(10, 100)
</code_context>
<issue_to_address>
**issue (complexity):** Consider refactoring the hashing logic into a reusable Murmur32 helper and replacing ctypes with pure bit-masked arithmetic to simplify get_sharding_key.

You can simplify the implementation without changing behaviour by:

1. Removing `ctypes` and using masking for 32‑bit arithmetic.
2. Extracting the Murmur32 logic into a helper.
3. Separating the “random when `user_id is None`” behaviour from the hashing.

For example:

```python
_C1 = 0xCC9E2D51
_C2 = 0x1B873593

def _imul(a: int, b: int) -> int:
    # 32‑bit multiply with wraparound
    return ((a & 0xFFFFFFFF) * (b & 0xFFFFFFFF)) & 0xFFFFFFFF

def _rotl32(x: int, r: int) -> int:
    x &= 0xFFFFFFFF
    return ((x << r) | (x >> (32 - r))) & 0xFFFFFFFF

def _murmur32(data: bytes, seed: int = 151488) -> int:
    length = len(data)
    r = seed

    for o in range(length // 4):
        i = 4 * o
        u = (
            data[i]
            | (data[i + 1] << 8)
            | (data[i + 2] << 16)
            | (data[i + 3] << 24)
        )
        u = _imul(u, _C1)
        u = _rotl32(u, 15)
        u = _imul(u, _C2)
        r ^= u
        r = _rotl32(r, 13)
        r = (_imul(r, 5) + 0xE6546B64) & 0xFFFFFFFF  # replaces ctypes.c_int32(...).value

    s = 4 * (length // 4)
    c = 0
    rem = length % 4
    if rem >= 3:
        c ^= data[s + 2] << 16
    if rem >= 2:
        c ^= data[s + 1] << 8
    if rem >= 1:
        c ^= data[s]
        c = _imul(c, _C1)
        c = _rotl32(c, 15)
        c = _imul(c, _C2)
        r ^= c

    r ^= length
    r &= 0xFFFFFFFF
    r ^= r >> 16
    r = _imul(r, 0x85EBCA6B) & 0xFFFFFFFF
    r ^= r >> 13
    r = _imul(r, 0xC2B2AE35) & 0xFFFFFFFF
    r ^= r >> 16
    return r
```

Then `get_sharding_key` becomes a thin wrapper with clearer behaviour:

```python
def get_sharding_key(user_id: str | None = None) -> int:
    if user_id is None:
        return random.randint(10, 100)
    h = _murmur32(user_id.encode("utf-8"))
    return (h % 100) + 1
```

This keeps the same hashing and random behaviour while:

- Dropping `ctypes` in favour of pure‑Python bit‑masking.
- Making the Murmur‑style hash reusable and easier to test.
- Reducing cognitive load in `get_sharding_key` by separating concerns.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/xhshow/core/xrap.py
Comment on lines +102 to +105
def _to_compact_json(data: Mapping[str, Any] | str | bytes | bytearray) -> str:
if isinstance(data, str):
return data
if isinstance(data, bytes | bytearray):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): 在 isinstance() 中使用类型元组,而不是 PEP 604 union

isinstance(data, bytes | bytearray) 会抛出 TypeError,因为 isinstance 的第二个参数必须是一个类型或类型元组,而不能是 PEP 604 union。这会在运行时破坏对 bytes/bytearray 的处理。请将其改为 isinstance(data, (bytes, bytearray))

Original comment in English

issue (bug_risk): Use a tuple of types in isinstance() instead of a PEP 604 union

isinstance(data, bytes | bytearray) will raise a TypeError because isinstance requires a type or a tuple of types as its second argument, not a PEP 604 union. This will break bytes/bytearray handling at runtime. Please change it to isinstance(data, (bytes, bytearray)).

Comment thread tests/test_xrap.py
Comment on lines +16 to +25
def test_xrap_param_packet_shape():
value = x_rap_param(
"//edith.xiaohongshu.com/api/sns/web/v1/homefeed",
'{"a":1}',
aes_key="wapilabkmyv4wl46",
random_string="mdzz94",
inner_key="h9w3tl5em3w4t67c",
timestamp_ms=0x0000019EB07ACDB2,
gzip_mtime=0x6A291532,
body_encry_time=69,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): 当所有随机因素都被固定时,通过断言完全确定性的输出来增强 x_rap_param 测试。

由于 test_xrap_param_packet_shape 中所有随机输入都是固定的,得到的 x-rap-param 应该是完全确定性的。与其只检查少量头部字段和盐值,建议同时断言完整的 base64 字符串或完整解码后的 payload,以捕获在主体结构、gzip 标志、加密或头部布局上的细微变化。如果完整字符串太冗长,可以只断言头部加上剩余部分的校验和(例如 xxh32),同样可以增强回归测试覆盖。

建议实现如下:

def test_xrap_param_packet_shape():
    value = x_rap_param(
        "//edith.xiaohongshu.com/api/sns/web/v1/homefeed",
        '{"a":1}',
        aes_key="wapilabkmyv4wl46",
        random_string="mdzz94",
        inner_key="h9w3tl5em3w4t67c",
        timestamp_ms=0x0000019EB07ACDB2,
        gzip_mtime=0x6A291532,
        body_encry_time=69,
        body_rand32=0xF95AD1C7,
        mask=0x65,
    )
    raw = base64.b64decode(value)

    # header layout checks
    assert raw[:4].hex() == "07240106"
    assert int.from_bytes(raw[4:8], "big") == 1
    assert int.from_bytes(raw[8:12], "big") == 20
    assert raw[36:42] == b"mdzz94"

    # full-packet regression check: base64 output must be fully deterministic
    assert value == EXPECTED_XRAP_PARAM_B64

    # strengthen coverage further with a checksum of the encrypted body payload
    body = raw[12:]
    assert zlib.crc32(body) == EXPECTED_XRAP_PARAM_BODY_CRC32

为了完整接入这一改动,你需要:

  1. tests/test_xrap.py 顶部导入 zlib
    • import zlib
  2. 在文件顶部(或测试附近)定义确定性期望值:
    • EXPECTED_XRAP_PARAM_B64 = "<fill-with-observed-x_rap_param-output>"
    • EXPECTED_XRAP_PARAM_BODY_CRC32 = <fill-with-int-crc32-of-raw[12:]>
  3. 获取这些值的方式:
    • 临时在测试中打印 valuezlib.crc32(base64.b64decode(value)[12:]),运行一次测试,将打印结果复制到上述常量中,然后删除打印语句。
  4. 填写完成后,测试会同时断言完整的 base64 编码 x-rap-param 以及剩余 payload 的校验和,从而提供你所期望的更强回归覆盖。
Original comment in English

suggestion (testing): Strengthen x_rap_param test by asserting the full deterministic output when all randomness is fixed.

Since all random inputs in test_xrap_param_packet_shape are fixed, the resulting x-rap-param should be fully deterministic. Rather than only checking a few header fields and the salt, please also assert the complete base64 string or the full decoded payload to catch subtle changes in body structure, gzip flags, encryption, or header layout. If the full string is unwieldy, asserting the header plus a checksum (e.g., xxh32) of the remainder would still strengthen regression coverage.

Suggested implementation:

def test_xrap_param_packet_shape():
    value = x_rap_param(
        "//edith.xiaohongshu.com/api/sns/web/v1/homefeed",
        '{"a":1}',
        aes_key="wapilabkmyv4wl46",
        random_string="mdzz94",
        inner_key="h9w3tl5em3w4t67c",
        timestamp_ms=0x0000019EB07ACDB2,
        gzip_mtime=0x6A291532,
        body_encry_time=69,
        body_rand32=0xF95AD1C7,
        mask=0x65,
    )
    raw = base64.b64decode(value)

    # header layout checks
    assert raw[:4].hex() == "07240106"
    assert int.from_bytes(raw[4:8], "big") == 1
    assert int.from_bytes(raw[8:12], "big") == 20
    assert raw[36:42] == b"mdzz94"

    # full-packet regression check: base64 output must be fully deterministic
    assert value == EXPECTED_XRAP_PARAM_B64

    # strengthen coverage further with a checksum of the encrypted body payload
    body = raw[12:]
    assert zlib.crc32(body) == EXPECTED_XRAP_PARAM_BODY_CRC32

To fully wire this change you should:

  1. Import zlib at the top of tests/test_xrap.py:
    • import zlib
  2. Define deterministic expectations near the top of the file (or next to the tests):
    • EXPECTED_XRAP_PARAM_B64 = "<fill-with-observed-x_rap_param-output>"
    • EXPECTED_XRAP_PARAM_BODY_CRC32 = <fill-with-int-crc32-of-raw[12:]>
  3. To obtain these values:
    • Temporarily print value and zlib.crc32(base64.b64decode(value)[12:]) from the test, run it once, copy the printed values into the constants, and then remove the prints.
  4. Once filled in, the test will assert both the complete base64-encoded x-rap-param and a checksum of the remaining payload, providing the strengthened regression coverage you requested.

Comment thread tests/test_xrap.py
Comment on lines +1 to +4
import base64

from xhshow.core.xrap import _encrypt_session_key, encrypt_block16, x_rap_param
from xhshow.utils.hash import xxh32 # noqa: F401

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): 为其他新引入的公共 API(搜索 ID、a1/web_id、分片 key,以及带 x-rap/xy-direction 的 sign_headers)添加测试。

本次改动引入了多个新的对外行为(generate_a1generate_web_idgenerate_search_idgenerate_search_request_idget_sharding_key,以及更新后的 sign_headers/sign_headers_get/sign_headers_post),目前尚缺乏测试覆盖。请添加测试以:

  • 验证 generate_a1 的输出长度和确定性(52 字符)以及 generate_web_id 的输出(从已知 a1 派生出的 32 位十六进制字符串)。
  • 检查 generate_search_id / generate_search_request_id 的格式(base36、random-timestamp 模式)以及在固定时间戳下的单调行为。
  • 确认 get_sharding_key 的确定性、结果范围在 1–100 之间,以及当 user_id=None 时的行为。
  • 确保 sign_headers(以及 GET/POST 包装函数)会设置 x-mns="unload",从 user_id 计算 xy-direction,并在 x_rap=True 时有条件地添加 x-rap-param
    这些测试将保护新的公共 API,防止高层 x-rap 行为发生回归。
Original comment in English

suggestion (testing): Add tests for other newly introduced public APIs (search IDs, a1/web_id, sharding key, and sign_headers x-rap/xy-direction).

This change adds several new public-facing behaviors (generate_a1, generate_web_id, generate_search_id, generate_search_request_id, get_sharding_key, and updated sign_headers/sign_headers_get/sign_headers_post) that currently lack test coverage. Please add tests to:

  • Verify output length and determinism for generate_a1 (52 chars) and generate_web_id (32-char hex from a known a1).
  • Check generate_search_id / generate_search_request_id formats (base36, random-timestamp pattern) and monotonic behavior with fixed timestamps.
  • Confirm get_sharding_key determinism, 1–100 range, and behavior when user_id=None.
  • Ensure sign_headers (and GET/POST wrappers) set x-mns="unload", compute xy-direction from user_id, and conditionally add x-rap-param when x_rap=True.
    This will guard the new public APIs and prevent regressions in higher-level x-rap behavior.

return ((x << r) | (x >> (32 - r))) & 0xFFFFFFFF


def get_sharding_key(user_id: str | None = None) -> int:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): 考虑将哈希逻辑重构为一个可复用的 Murmur32 辅助函数,并用纯位掩码算术替代 ctypes,从而简化 get_sharding_key。

在不改变行为的前提下,你可以通过以下方式简化实现:

  1. 移除 ctypes,用 32 位掩码运算来完成算术。
  2. 将 Murmur32 逻辑提取到一个辅助函数中。
  3. 将“当 user_id is None 时随机”这一行为从哈希逻辑中分离出来。

例如:

_C1 = 0xCC9E2D51
_C2 = 0x1B873593

def _imul(a: int, b: int) -> int:
    # 32‑bit multiply with wraparound
    return ((a & 0xFFFFFFFF) * (b & 0xFFFFFFFF)) & 0xFFFFFFFF

def _rotl32(x: int, r: int) -> int:
    x &= 0xFFFFFFFF
    return ((x << r) | (x >> (32 - r))) & 0xFFFFFFFF

def _murmur32(data: bytes, seed: int = 151488) -> int:
    length = len(data)
    r = seed

    for o in range(length // 4):
        i = 4 * o
        u = (
            data[i]
            | (data[i + 1] << 8)
            | (data[i + 2] << 16)
            | (data[i + 3] << 24)
        )
        u = _imul(u, _C1)
        u = _rotl32(u, 15)
        u = _imul(u, _C2)
        r ^= u
        r = _rotl32(r, 13)
        r = (_imul(r, 5) + 0xE6546B64) & 0xFFFFFFFF  # replaces ctypes.c_int32(...).value

    s = 4 * (length // 4)
    c = 0
    rem = length % 4
    if rem >= 3:
        c ^= data[s + 2] << 16
    if rem >= 2:
        c ^= data[s + 1] << 8
    if rem >= 1:
        c ^= data[s]
        c = _imul(c, _C1)
        c = _rotl32(c, 15)
        c = _imul(c, _C2)
        r ^= c

    r ^= length
    r &= 0xFFFFFFFF
    r ^= r >> 16
    r = _imul(r, 0x85EBCA6B) & 0xFFFFFFFF
    r ^= r >> 13
    r = _imul(r, 0xC2B2AE35) & 0xFFFFFFFF
    r ^= r >> 16
    return r

然后 get_sharding_key 可以变成一个行为更清晰的薄封装:

def get_sharding_key(user_id: str | None = None) -> int:
    if user_id is None:
        return random.randint(10, 100)
    h = _murmur32(user_id.encode("utf-8"))
    return (h % 100) + 1

这样可以在保持哈希逻辑和随机行为不变的同时:

  • 去掉 ctypes,改用纯 Python 位掩码运算;
  • 让 Murmur 风格的哈希可复用且更易于测试;
  • 通过分离关注点,降低 get_sharding_key 的理解复杂度。
Original comment in English

issue (complexity): Consider refactoring the hashing logic into a reusable Murmur32 helper and replacing ctypes with pure bit-masked arithmetic to simplify get_sharding_key.

You can simplify the implementation without changing behaviour by:

  1. Removing ctypes and using masking for 32‑bit arithmetic.
  2. Extracting the Murmur32 logic into a helper.
  3. Separating the “random when user_id is None” behaviour from the hashing.

For example:

_C1 = 0xCC9E2D51
_C2 = 0x1B873593

def _imul(a: int, b: int) -> int:
    # 32‑bit multiply with wraparound
    return ((a & 0xFFFFFFFF) * (b & 0xFFFFFFFF)) & 0xFFFFFFFF

def _rotl32(x: int, r: int) -> int:
    x &= 0xFFFFFFFF
    return ((x << r) | (x >> (32 - r))) & 0xFFFFFFFF

def _murmur32(data: bytes, seed: int = 151488) -> int:
    length = len(data)
    r = seed

    for o in range(length // 4):
        i = 4 * o
        u = (
            data[i]
            | (data[i + 1] << 8)
            | (data[i + 2] << 16)
            | (data[i + 3] << 24)
        )
        u = _imul(u, _C1)
        u = _rotl32(u, 15)
        u = _imul(u, _C2)
        r ^= u
        r = _rotl32(r, 13)
        r = (_imul(r, 5) + 0xE6546B64) & 0xFFFFFFFF  # replaces ctypes.c_int32(...).value

    s = 4 * (length // 4)
    c = 0
    rem = length % 4
    if rem >= 3:
        c ^= data[s + 2] << 16
    if rem >= 2:
        c ^= data[s + 1] << 8
    if rem >= 1:
        c ^= data[s]
        c = _imul(c, _C1)
        c = _rotl32(c, 15)
        c = _imul(c, _C2)
        r ^= c

    r ^= length
    r &= 0xFFFFFFFF
    r ^= r >> 16
    r = _imul(r, 0x85EBCA6B) & 0xFFFFFFFF
    r ^= r >> 13
    r = _imul(r, 0xC2B2AE35) & 0xFFFFFFFF
    r ^= r >> 16
    return r

Then get_sharding_key becomes a thin wrapper with clearer behaviour:

def get_sharding_key(user_id: str | None = None) -> int:
    if user_id is None:
        return random.randint(10, 100)
    h = _murmur32(user_id.encode("utf-8"))
    return (h % 100) + 1

This keeps the same hashing and random behaviour while:

  • Dropping ctypes in favour of pure‑Python bit‑masking.
  • Making the Murmur‑style hash reusable and easier to test.
  • Reducing cognitive load in get_sharding_key by separating concerns.

@Cloxl
Cloxl merged commit 5f45309 into master Jun 11, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant