fix: validate renderDefaults fields and reject non-finite calibration values
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
799998fad2
commit
f22a0c16df
2 changed files with 38 additions and 5 deletions
|
|
@ -40,4 +40,24 @@ describe('loadConfig', () => {
|
||||||
mockFetch({}, false);
|
mockFetch({}, false);
|
||||||
await expect(loadConfig('config.json')).rejects.toThrow();
|
await expect(loadConfig('config.json')).rejects.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('ignores malformed renderDefaults fields, keeping defaults and dropping extras', async () => {
|
||||||
|
mockFetch({ streamUrl: 'x', renderDefaults: { lineWidth: 'thick', cutColor: '#abc', bogus: true } });
|
||||||
|
const cfg = await loadConfig('config.json');
|
||||||
|
expect(cfg.renderDefaults.cutColor).toBe('#abc'); // valid string kept
|
||||||
|
expect(cfg.renderDefaults.lineWidth).toBe(DEFAULT_RENDER.lineWidth); // invalid number → default
|
||||||
|
expect(cfg.renderDefaults.rapidColor).toBe(DEFAULT_RENDER.rapidColor); // missing → default
|
||||||
|
expect(cfg.renderDefaults).not.toHaveProperty('bogus'); // extra key dropped
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects a calibration whose homography contains NaN', async () => {
|
||||||
|
const calibration = {
|
||||||
|
imagePoints: [[0, 0], [1, 0], [1, 1], [0, 1]],
|
||||||
|
machinePoints: [[0, 0], [1, 0], [1, 1], [0, 1]],
|
||||||
|
homography: [1, 0, 0, 0, 1, 0, 0, 0, NaN],
|
||||||
|
};
|
||||||
|
mockFetch({ streamUrl: 'x', calibration });
|
||||||
|
const cfg = await loadConfig('config.json');
|
||||||
|
expect(cfg.calibration).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
import type { AppConfig, Calibration, Mat3, Vec2 } from './types';
|
import type { AppConfig, Calibration, Mat3, Vec2, RenderStyle } from './types';
|
||||||
|
|
||||||
export const DEFAULT_RENDER = { cutColor: '#00e5ff', rapidColor: '#ff9800', lineWidth: 1.5 };
|
export const DEFAULT_RENDER: RenderStyle = { cutColor: '#00e5ff', rapidColor: '#ff9800', lineWidth: 1.5 };
|
||||||
|
|
||||||
|
function isFiniteNumber(n: unknown): n is number {
|
||||||
|
return typeof n === 'number' && Number.isFinite(n);
|
||||||
|
}
|
||||||
|
|
||||||
function isVec2Array(v: unknown): v is Vec2[] {
|
function isVec2Array(v: unknown): v is Vec2[] {
|
||||||
return Array.isArray(v) && v.every((p) => Array.isArray(p) && p.length === 2 && p.every((n) => typeof n === 'number'));
|
return Array.isArray(v) && v.every((p) => Array.isArray(p) && p.length === 2 && p.every(isFiniteNumber));
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseCalibration(c: unknown): Calibration | null {
|
function parseCalibration(c: unknown): Calibration | null {
|
||||||
|
|
@ -11,7 +15,7 @@ function parseCalibration(c: unknown): Calibration | null {
|
||||||
const obj = c as Record<string, unknown>;
|
const obj = c as Record<string, unknown>;
|
||||||
if (!isVec2Array(obj.imagePoints) || !isVec2Array(obj.machinePoints)) return null;
|
if (!isVec2Array(obj.imagePoints) || !isVec2Array(obj.machinePoints)) return null;
|
||||||
if (!Array.isArray(obj.homography) || obj.homography.length !== 9) return null;
|
if (!Array.isArray(obj.homography) || obj.homography.length !== 9) return null;
|
||||||
if (!obj.homography.every((n) => typeof n === 'number')) return null;
|
if (!obj.homography.every(isFiniteNumber)) return null;
|
||||||
if (obj.imagePoints.length !== obj.machinePoints.length || obj.imagePoints.length < 4) return null;
|
if (obj.imagePoints.length !== obj.machinePoints.length || obj.imagePoints.length < 4) return null;
|
||||||
return {
|
return {
|
||||||
imagePoints: obj.imagePoints,
|
imagePoints: obj.imagePoints,
|
||||||
|
|
@ -20,6 +24,15 @@ function parseCalibration(c: unknown): Calibration | null {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseRenderStyle(r: unknown): RenderStyle {
|
||||||
|
const obj = (r && typeof r === 'object' ? r : {}) as Record<string, unknown>;
|
||||||
|
return {
|
||||||
|
cutColor: typeof obj.cutColor === 'string' ? obj.cutColor : DEFAULT_RENDER.cutColor,
|
||||||
|
rapidColor: typeof obj.rapidColor === 'string' ? obj.rapidColor : DEFAULT_RENDER.rapidColor,
|
||||||
|
lineWidth: isFiniteNumber(obj.lineWidth) ? obj.lineWidth : DEFAULT_RENDER.lineWidth,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function loadConfig(url: string): Promise<AppConfig> {
|
export async function loadConfig(url: string): Promise<AppConfig> {
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
if (!res.ok) throw new Error(`Failed to load ${url}: ${res.status}`);
|
if (!res.ok) throw new Error(`Failed to load ${url}: ${res.status}`);
|
||||||
|
|
@ -27,6 +40,6 @@ export async function loadConfig(url: string): Promise<AppConfig> {
|
||||||
return {
|
return {
|
||||||
streamUrl: typeof raw.streamUrl === 'string' ? raw.streamUrl : '',
|
streamUrl: typeof raw.streamUrl === 'string' ? raw.streamUrl : '',
|
||||||
calibration: parseCalibration(raw.calibration),
|
calibration: parseCalibration(raw.calibration),
|
||||||
renderDefaults: { ...DEFAULT_RENDER, ...(raw.renderDefaults as object | undefined) },
|
renderDefaults: parseRenderStyle(raw.renderDefaults),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue