跨端: 补 MailSummary 的解析边界兜底(列表这条主流的 omitempty 一直是敞的)
判决书来自 `harmony-arkts` 那条判据,它在我加了授权栏历史之后报出两处调用点:
MainPage.ets: parts.push(e.reason.length > 0 ? …) ← 假阳性
MainPage.ets: if (m.mail_type === 'permission_request' && m.permission_result.length > 0) ← 真 bug
## 真 bug:`MailSummary` 没有解析边界兜底
本仓早就为这个形状付过代价,**而且修法只修了一半**:
· `MailDetail` 有 `normalize()`,并在 `MailApi.mailDetail()` 接了(当时那次是**整页白屏**);
· 而列表用的 `MailSummary` **没有** —— 于是 `mail_type` / `permission_result` /
`session_alias` 这些带 `omitempty` 的字段在缺失时是 `undefined`,
而三处调用点直接读 `.length`。
服务端 `omitempty` 的语义是**整个 key 不出现**(不是给空串),
ArkTS 裸 cast(`JSON.parse(raw) as T`)缺键给 `undefined`、**不会**应用类里那个 `= ''`。
⇒ 这是同一形状的**第四次**(前三次:`MailDetail` 白屏、`participantAddress` 的 trim、
`AddressSuggestion.title`)。前三次都是"读的人临时守一下",
**而列表这条主流一直敞着**。
修法(与 `MailDetail` 同一处、同一纪律):给 `MailSummary` 加 `normalize()`,
在 `MailApi.inbox()` / `sent()` / `sessionMails()` **三处**解析边界接上。
★ 为什么不在调用点加 `??`:`MailSummary` 上还有 `mail_type`/`session_alias`
同样带 omitempty —— 逐个调用点加就是"每加一处就得记得做一次",
本仓反复在消的形状。归一化做一次、覆盖全部字段。
## 假阳性:同名词撞车
`e.reason` 的 `e` 是 **`AccountError`** —— 鸿蒙**本地类**(`MailStore.ets:73`),
只经 `AccountError.of(account, reason)` 构造 ⇒ `reason` 恒为 string。
而判据收集的那个 `json:"reason,omitempty"` 属于 `RenameProposal`
(`rename_proposal.go:39`,**完全不同的**接口)。已按既有
「已逐个核实过的豁免」格式加 ⑤,附取证。
## 期间修了判据自己的一个洞(变异实测)
我给 ⑥ 加豁免时第一版写的是**无条件**正则 —— 变异实测
(把 `MailSummary.normalize` 里那行 `m.permission_result = str(...)` 删掉)
**判据照样全绿**:豁免把那一行永久致盲了。
这正是本仓反复消的形状:**豁免口自己没人管**。
⇒ 改成"**有前提**的豁免":先断言 `MailSummary.normalize` 里确实有那两行,
命中才豁免。变异复验:
删掉 normalize 里 permission_result → 判据红 ✓
删掉 normalize 里 mail_type → 判据红 ✓
两者都在 → 全绿 ✓
⇒ 豁免表达的是"**因为上游归一了**,所以这里可以不写 ?? ",
而不是"这一行不用管"。
This commit is contained in:
@ -167,7 +167,19 @@ export class MailApi {
|
||||
/** 收件箱(status + limit) */
|
||||
async inbox(status: string, limit: number): Promise<InboxResponse> {
|
||||
const query: string = 'status=' + status + '&limit=' + limit;
|
||||
return this.client.get<InboxResponse>('/me/mail/inbox', query);
|
||||
const resp: InboxResponse = await this.client.get<InboxResponse>('/me/mail/inbox', query);
|
||||
/*
|
||||
* ★★ 2026-09-21 补归一化 —— 与 `mailDetail()` 同一处、同一理由:
|
||||
* 服务端 `mail_type` / `permission_result` / `session_alias` 带 `omitempty`,
|
||||
* 零值时**整个 key 不出现**,而这里是裸 `JSON.parse as T` ⇒ 缺失字段是
|
||||
* `undefined`,下游 `.length` 当场抛。
|
||||
* 此前只给 `MailDetail` 归了,**列表这条主流一直敞着**
|
||||
* (判据 `harmony-arkts` 报的就是它)。
|
||||
*/
|
||||
for (let i = 0; i < resp.mails.length; i++) {
|
||||
resp.mails[i] = MailSummary.normalize(resp.mails[i]);
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -175,7 +187,12 @@ export class MailApi {
|
||||
* (收件箱的 `total` 是未读数,见 docs/API.md 的「同名不同义」),这里也不读它。
|
||||
*/
|
||||
async sent(): Promise<SentResponse> {
|
||||
return this.client.get<SentResponse>('/me/mail/sent');
|
||||
const resp: SentResponse = await this.client.get<SentResponse>('/me/mail/sent');
|
||||
/* 与 `inbox()` 同一处归一化(发件箱回包同样是裸 cast) */
|
||||
for (let i = 0; i < resp.mails.length; i++) {
|
||||
resp.mails[i] = MailSummary.normalize(resp.mails[i]);
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -262,6 +279,10 @@ export class MailApi {
|
||||
const resp: SessionMailsResponse = await this.client.get<SessionMailsResponse>(
|
||||
'/sessions/' + sessionId + '/mails'
|
||||
);
|
||||
/* 与 `inbox()` 同一处归一化 */
|
||||
for (let i = 0; i < resp.mails.length; i++) {
|
||||
resp.mails[i] = MailSummary.normalize(resp.mails[i]);
|
||||
}
|
||||
return resp.mails;
|
||||
}
|
||||
|
||||
|
||||
@ -111,6 +111,52 @@ export class MailSummary implements MailLike {
|
||||
/** 客户端聚合字段:服务端不返回,由收件箱按来源账号填充。 */
|
||||
source_account_id: string = '';
|
||||
source_account_name: string = '';
|
||||
|
||||
/**
|
||||
* **列表行**的解析边界兜底(与 `MailDetail.normalize` 同一职责、同一个理由)。
|
||||
*
|
||||
* ★★ 2026-09-21 补。此前**只有 `MailDetail` 有 normalize**,而列表用的
|
||||
* `MailSummary` 没有 —— 于是带 `omitempty` 的字段在缺失时是 `undefined`,
|
||||
* 而调用点直接写了 `.length`:
|
||||
*
|
||||
* MainPage.ets m.permission_result.length > 0
|
||||
* MailGrouping.ts !m.permission_result
|
||||
* MainPage.ets c.session_alias.length > 0
|
||||
*
|
||||
* 服务端 `omitempty` 的语义是**整个 key 不出现**(不是给空串),
|
||||
* 而 ArkTS 的裸 cast(`JSON.parse(raw) as T`)缺键时给 `undefined`、
|
||||
* **不会**应用类里那个 `= ''` ⇒ 读 `.length` 抛
|
||||
* `Cannot read property length of undefined`。
|
||||
*
|
||||
* ⇒ 这是本仓**第四次**同一形状(前三次:`MailDetail` 白屏、
|
||||
* `participantAddress` 的 trim、`AddressSuggestion.title`)。
|
||||
* 前三次都是"读的人临时守一下",而**列表这条主流一直是敞着的**。
|
||||
*
|
||||
* ★ 钉住它的判据:`harmony-arkts.test.mjs` 的
|
||||
* 「服务端『只以 omitempty 形式出现』的字段:客户端读它时必须有 `??` 兜底」
|
||||
* —— 正是它把这个漏报出来的(它报的是两处调用点,根因是这里缺 normalize)。
|
||||
*
|
||||
* 调用点:`MailApi.inbox()` / `MailApi.sessionMails()`(解析边界)。
|
||||
*/
|
||||
static normalize(m: MailSummary): MailSummary {
|
||||
m.mail_id = str(m.mail_id);
|
||||
m.session_id = str(m.session_id);
|
||||
m.session_alias = str(m.session_alias);
|
||||
m.from_name = str(m.from_name);
|
||||
m.to_name = str(m.to_name);
|
||||
m.subject = str(m.subject);
|
||||
m.body_preview = str(m.body_preview);
|
||||
m.created_at = str(m.created_at);
|
||||
m.status = str(m.status);
|
||||
m.is_read = boolOr(m.is_read, true);
|
||||
m.permission_mode = str(m.permission_mode);
|
||||
/* 两个 `omitempty` 字段(缺失时是 undefined)—— 判据命中的就是它们 */
|
||||
m.mail_type = str(m.mail_type);
|
||||
m.permission_result = str(m.permission_result);
|
||||
m.attachments = m.attachments === undefined || m.attachments === null ? [] : m.attachments;
|
||||
m.cc_list = m.cc_list === undefined || m.cc_list === null ? [] : m.cc_list;
|
||||
return m;
|
||||
}
|
||||
}
|
||||
|
||||
/** 附件元数据 */
|
||||
|
||||
Reference in New Issue
Block a user