fix(protocol): validate profile birthdays against the calendar

Omit invalid birthdays and preserve valid leap days when the birth year is private.

Refs #594
This commit is contained in:
DrSmoothl
2026-10-31 18:28:05 +08:00
parent a557bf72e7
commit d8763c98db
2 changed files with 17 additions and 5 deletions
@@ -94,7 +94,7 @@ export function applyProfileDetails(info: UserProfileInfo, numMap: Map<number, n
export function applyBirthday(info: UserProfileInfo, bytesMap: Map<number, Uint8Array>): void {
const raw = bytesMap.get(20031);
if (!raw || raw.length < 4) {
if (raw?.length !== 4) {
return;
}
@@ -103,7 +103,11 @@ export function applyBirthday(info: UserProfileInfo, bytesMap: Map<number, Uint8
const month = bytes.readUInt8(2);
const day = bytes.readUInt8(3);
if (month < 1 || month > 12 || day < 1 || day > 31) {
// A hidden year can still represent February 29.
const date = new Date(0);
date.setUTCFullYear(year || 2000, month - 1, day);
if (date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) {
return;
}
@@ -148,17 +148,22 @@ describe('FetchUserProfile namespace', () => {
},
);
it('omits only the birth year when the year is hidden', async () => {
it.each([
[12, 31],
[2, 29],
])('keeps a hidden-year birthday %i/%i', async (month, day) => {
const sender = makeSender({
body: {
uin: 10001,
properties: {
bytesProperties: [{ code: 20031, value: new Uint8Array([0, 0, 12, 31]) }],
bytesProperties: [
{ code: 20031, value: new Uint8Array([0, 0, month, day]) },
],
},
},
});
const profile = await FetchUserProfile.invoke(sender, { uin: 10001 });
expect(profile).toMatchObject({ birthdayMonth: 12, birthdayDay: 31 });
expect(profile).toMatchObject({ birthdayMonth: month, birthdayDay: day });
expect(profile).not.toHaveProperty('birthdayYear');
});
@@ -167,7 +172,10 @@ describe('FetchUserProfile namespace', () => {
['an all-zero value', [0, 0, 0, 0]],
['an out-of-range month', [0x07, 0xd0, 13, 1]],
['an out-of-range day', [0x07, 0xd0, 1, 32]],
['April 31', [0x07, 0xd0, 4, 31]],
['a leap day in 1900', [0x07, 0x6c, 2, 29]],
['a truncated value', [0x07, 0xd0, 3]],
['an oversized value', [0x07, 0xd0, 3, 15, 0]],
])('leaves the birthday unavailable for %s', async (_case, value) => {
const sender = makeSender({
body: {