Feat/xrap param - #108
Conversation
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?}
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your Experience访问你的 dashboard 以:
Getting HelpOriginal review guide in EnglishReviewer's GuideAdds 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-paramsequenceDiagram
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?}
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
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>帮我变得更有用!请在每条评论上点击 👍 或 👎,我会根据你的反馈改进评审质量。
Original comment in English
Hey - I've found 4 issues, and left some high level feedback:
- In
core/xrap.pythere 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
xxh32implementation inutils/hash.pypulls_MASK_32from a newCryptoConfig()instance but otherwise uses only static literals; replacing this with a simple0xFFFFFFFF(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>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| def _to_compact_json(data: Mapping[str, Any] | str | bytes | bytearray) -> str: | ||
| if isinstance(data, str): | ||
| return data | ||
| if isinstance(data, bytes | bytearray): |
There was a problem hiding this comment.
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)).
| 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, |
There was a problem hiding this comment.
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为了完整接入这一改动,你需要:
- 在
tests/test_xrap.py顶部导入zlib:import zlib
- 在文件顶部(或测试附近)定义确定性期望值:
EXPECTED_XRAP_PARAM_B64 = "<fill-with-observed-x_rap_param-output>"EXPECTED_XRAP_PARAM_BODY_CRC32 = <fill-with-int-crc32-of-raw[12:]>
- 获取这些值的方式:
- 临时在测试中打印
value和zlib.crc32(base64.b64decode(value)[12:]),运行一次测试,将打印结果复制到上述常量中,然后删除打印语句。
- 临时在测试中打印
- 填写完成后,测试会同时断言完整的 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_CRC32To fully wire this change you should:
- Import
zlibat the top oftests/test_xrap.py:import zlib
- 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:]>
- To obtain these values:
- Temporarily print
valueandzlib.crc32(base64.b64decode(value)[12:])from the test, run it once, copy the printed values into the constants, and then remove the prints.
- Temporarily print
- 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.
| import base64 | ||
|
|
||
| from xhshow.core.xrap import _encrypt_session_key, encrypt_block16, x_rap_param | ||
| from xhshow.utils.hash import xxh32 # noqa: F401 |
There was a problem hiding this comment.
suggestion (testing): 为其他新引入的公共 API(搜索 ID、a1/web_id、分片 key,以及带 x-rap/xy-direction 的 sign_headers)添加测试。
本次改动引入了多个新的对外行为(generate_a1、generate_web_id、generate_search_id、generate_search_request_id、get_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) andgenerate_web_id(32-char hex from a known a1). - Check
generate_search_id/generate_search_request_idformats (base36,random-timestamppattern) and monotonic behavior with fixed timestamps. - Confirm
get_sharding_keydeterminism, 1–100 range, and behavior whenuser_id=None. - Ensure
sign_headers(and GET/POST wrappers) setx-mns="unload", computexy-directionfromuser_id, and conditionally addx-rap-paramwhenx_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: |
There was a problem hiding this comment.
issue (complexity): 考虑将哈希逻辑重构为一个可复用的 Murmur32 辅助函数,并用纯位掩码算术替代 ctypes,从而简化 get_sharding_key。
在不改变行为的前提下,你可以通过以下方式简化实现:
- 移除
ctypes,用 32 位掩码运算来完成算术。 - 将 Murmur32 逻辑提取到一个辅助函数中。
- 将“当
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:
- Removing
ctypesand using masking for 32‑bit arithmetic. - Extracting the Murmur32 logic into a helper.
- 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 rThen 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) + 1This keeps the same hashing and random behaviour while:
- Dropping
ctypesin favour of pure‑Python bit‑masking. - Making the Murmur‑style hash reusable and easier to test.
- Reducing cognitive load in
get_sharding_keyby separating concerns.
Summary by Sourcery
添加对生成 x-rap-param 请求头及相关分片/搜索工具的支持,更新加密配置,并通过精简示例和开发流程刷新文档。
New Features:
search_id和search_request_id生成辅助函数。a1和web_idCookie 值的静态辅助函数。sign_headersAPI 中。Enhancements:
Tests:
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:
Enhancements:
Tests: