From 3b3b92b25eb6942917796da9eee388a862a67582 Mon Sep 17 00:00:00 2001 From: sk8kpwhrjt-creator Date: Wed, 20 May 2026 21:37:39 +0800 Subject: [PATCH] fix: normalize zero viewport dimensions in records --- .../src/record-schema.ts | 71 +++++++- .../record-schema.test.ts | 156 ++++++++++++++++++ 2 files changed, 223 insertions(+), 4 deletions(-) create mode 100644 test/generator-networks-creator/record-schema.test.ts diff --git a/packages/generator-networks-creator/src/record-schema.ts b/packages/generator-networks-creator/src/record-schema.ts index bb572359..81dcfeab 100644 --- a/packages/generator-networks-creator/src/record-schema.ts +++ b/packages/generator-networks-creator/src/record-schema.ts @@ -84,6 +84,63 @@ const KNOWN_OS_FONTS = { ], }; +function isPositiveNumber(value: unknown): value is number { + return typeof value === 'number' && Number.isFinite(value) && value > 0; +} + +function normalizeScreenViewport(screen: any) { + if (!screen || typeof screen !== 'object') return screen; + + const normalized = { ...screen }; + const outerWidth = isPositiveNumber(normalized.outerWidth) + ? normalized.outerWidth + : normalized.width; + const outerHeight = isPositiveNumber(normalized.outerHeight) + ? normalized.outerHeight + : normalized.height; + const screenWidth = isPositiveNumber(normalized.width) + ? normalized.width + : outerWidth; + const screenHeight = isPositiveNumber(normalized.height) + ? normalized.height + : outerHeight; + const availableHeight = isPositiveNumber(normalized.availHeight) + ? normalized.availHeight + : screenHeight; + + if ( + !isPositiveNumber(normalized.innerWidth) && + isPositiveNumber(screenWidth) && + isPositiveNumber(outerWidth) + ) { + normalized.innerWidth = Math.min(screenWidth, outerWidth); + } + + if ( + !isPositiveNumber(normalized.innerHeight) && + isPositiveNumber(availableHeight) && + isPositiveNumber(outerHeight) + ) { + normalized.innerHeight = Math.min(availableHeight, outerHeight); + } + + if ( + !isPositiveNumber(normalized.clientWidth) && + isPositiveNumber(normalized.innerWidth) + ) { + normalized.clientWidth = normalized.innerWidth; + } + + if ( + !isPositiveNumber(normalized.clientHeight) && + isPositiveNumber(normalized.innerHeight) + ) { + normalized.clientHeight = normalized.innerHeight; + } + + return normalized; +} + export async function getRecordSchema() { const robotUserAgents = (await fetch( 'https://raw.githubusercontent.com/atmire/COUNTER-Robots/master/COUNTER_Robots_list.json', @@ -115,6 +172,12 @@ export async function getRecordSchema() { return { ...castRecord, + browserFingerprint: { + ...castRecord.browserFingerprint, + screen: normalizeScreenViewport( + castRecord.browserFingerprint.screen, + ), + }, userAgentProps: { parsedUserAgent, isDesktop: !['wearable', 'mobile'].includes( @@ -247,10 +310,10 @@ export async function getRecordSchema() { height: z.number().positive(), availWidth: z.number().positive(), availHeight: z.number().positive(), - clientWidth: z.number().nonnegative(), - clientHeight: z.number().nonnegative(), - innerWidth: z.number().nonnegative(), - innerHeight: z.number().nonnegative(), + clientWidth: z.number().positive(), + clientHeight: z.number().positive(), + innerWidth: z.number().positive(), + innerHeight: z.number().positive(), outerWidth: z.number().positive(), outerHeight: z.number().positive(), colorDepth: z.number().positive().optional(), diff --git a/test/generator-networks-creator/record-schema.test.ts b/test/generator-networks-creator/record-schema.test.ts new file mode 100644 index 00000000..9a40c90a --- /dev/null +++ b/test/generator-networks-creator/record-schema.test.ts @@ -0,0 +1,156 @@ +import { describe, expect, test, vi } from 'vitest'; + +import { getRecordSchema } from '../../packages/generator-networks-creator/src/record-schema'; + +vi.mock('node-fetch', () => ({ + default: vi.fn(async () => ({ + json: async () => [], + })), +})); + +const userAgent = + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36'; + +function createValidRecord() { + return { + requestFingerprint: { + headers: { + 'user-agent': userAgent, + }, + httpVersion: '2', + }, + browserFingerprint: { + webdriver: false, + appVersion: userAgent, + hardwareConcurrency: 8, + multimediaDevices: { + speakers: [], + micros: [], + webcams: [], + }, + extraProperties: {}, + plugins: [], + mimeTypes: [], + deviceMemory: 8, + oscpu: null, + battery: null, + platform: 'MacIntel', + doNotTrack: null, + audioCodecs: { + ogg: 'probably', + mp3: 'probably', + wav: 'probably', + m4a: 'maybe', + aac: 'probably', + }, + videoCodecs: { + ogg: 'probably', + h264: 'probably', + webm: 'probably', + }, + language: 'en-US', + languages: ['en-US'], + product: 'Gecko', + appName: 'Netscape', + appCodeName: 'Mozilla', + maxTouchPoints: 0, + productSub: '20030107', + vendor: 'Google Inc.', + vendorSub: '', + videoCard: { + vendor: 'Google Inc. (Intel)', + renderer: + 'ANGLE (Intel Inc., Intel(R) HD Graphics 630 OpenGL Engine)', + }, + fonts: [], + screen: { + availTop: 25, + availLeft: 0, + pageXOffset: 0, + pageYOffset: 0, + screenX: 0, + hasHDR: false, + width: 1440, + height: 900, + availWidth: 1440, + availHeight: 875, + clientWidth: 1284, + clientHeight: 769, + innerWidth: 1284, + innerHeight: 769, + outerWidth: 1284, + outerHeight: 858, + colorDepth: 24, + pixelDepth: 24, + devicePixelRatio: 2, + }, + userAgent, + }, + }; +} + +function withScreenValues(screen: Record) { + const record = createValidRecord(); + record.browserFingerprint.screen = { + ...record.browserFingerprint.screen, + ...screen, + }; + return record; +} + +describe('Record schema', () => { + test('accepts browser records with non-zero viewport dimensions', async () => { + const schema = await getRecordSchema(); + + expect(schema.safeParse(createValidRecord()).success).toBe(true); + }); + + test('normalizes zero viewport dimensions before validation', async () => { + const schema = await getRecordSchema(); + const result = schema.safeParse( + withScreenValues({ + clientWidth: 0, + clientHeight: 0, + innerWidth: 0, + innerHeight: 0, + }), + ); + + expect(result.success).toBe(true); + + if (result.success) { + expect( + result.data.browserFingerprint.screen.innerWidth, + ).toBeGreaterThan(0); + expect( + result.data.browserFingerprint.screen.innerHeight, + ).toBeGreaterThan(0); + expect(result.data.browserFingerprint.screen.clientWidth).toBe( + result.data.browserFingerprint.screen.innerWidth, + ); + expect(result.data.browserFingerprint.screen.clientHeight).toBe( + result.data.browserFingerprint.screen.innerHeight, + ); + } + }); + + test.each(['clientWidth', 'clientHeight', 'innerWidth', 'innerHeight'])( + 'rejects unfixable zero %s', + async (dimension) => { + const schema = await getRecordSchema(); + + expect( + schema.safeParse( + withScreenValues({ + outerWidth: 0, + outerHeight: 0, + width: 0, + height: 0, + availHeight: 0, + [dimension]: 0, + }), + ).success, + ).toBe(false); + }, + ); +});