Harden import and paste validation, fix duplicate exports and variable declarations

This commit is contained in:
drjones
2026-06-13 19:51:09 -07:00
parent 44d89d205c
commit 1302aae31c
10 changed files with 331 additions and 72 deletions

View File

@@ -1,4 +1,4 @@
import type { PlacedPart, ProjectFile, PartType } from '../types';
import type { PlacedPart, ProjectFile, PartType, Vec3 } from '../types';
import { PART_DEFS } from '../parts/catalog';
/**
@@ -15,6 +15,83 @@ export function toProjectFile(name: string, parts: PlacedPart[]): ProjectFile {
return { app: 'hydro-builder', version: 1, name, savedAt: new Date().toISOString(), parts };
}
/** Validate and sanitize a raw part object, throwing if critical fields are missing/corrupted. */
export function sanitizePart(raw: any): PlacedPart {
if (!raw || typeof raw !== 'object') {
throw new Error('Part data is not a valid JSON object.');
}
// Type validation
if (typeof raw.type !== 'string' || !(raw.type in PART_DEFS)) {
throw new Error(`Part has an invalid or unrecognized type: ${raw.type || 'undefined'}`);
}
const type = raw.type as PartType;
// ID validation
let id = raw.id;
if (typeof id !== 'string' || !id) {
id = Math.random().toString(36).substring(2, 10);
}
// Position validation
let position: Vec3;
if (Array.isArray(raw.position) && raw.position.length === 3) {
const coords = raw.position.map((num: any) => {
const val = Number(num);
return Number.isFinite(val) ? val : 0;
});
position = [coords[0], coords[1], coords[2]];
} else {
throw new Error(`Part ${id} (${type}) is missing a valid 3D position array.`);
}
// Rotation validation
let rotation: Vec3;
if (Array.isArray(raw.rotation) && raw.rotation.length === 3) {
const coords = raw.rotation.map((num: any) => {
const val = Number(num);
return Number.isFinite(val) ? val : 0;
});
rotation = [coords[0], coords[1], coords[2]];
} else {
throw new Error(`Part ${id} (${type}) is missing a valid 3D rotation array.`);
}
// Params validation
const defaultParams = PART_DEFS[type].defaults || {};
const rawParams = raw.params && typeof raw.params === 'object' ? raw.params : {};
const params: Record<string, number> = {};
// Fill in default values
for (const [key, defVal] of Object.entries(defaultParams)) {
params[key] = defVal;
}
// Override with raw values if they are finite numbers
for (const [key, rawVal] of Object.entries(rawParams)) {
const val = Number(rawVal);
if (Number.isFinite(val)) {
params[key] = val;
}
}
// Optional attributes
const color = typeof raw.color === 'string' && raw.color.startsWith('#') ? raw.color : undefined;
const label = typeof raw.label === 'string' ? raw.label : undefined;
const roomId = typeof raw.roomId === 'string' ? raw.roomId : undefined;
return {
id,
type,
position,
rotation,
params,
...(color ? { color } : {}),
...(label ? { label } : {}),
...(roomId ? { roomId } : {}),
};
}
/** Validate an arbitrary JSON value as a ProjectFile, dropping bad parts or throwing on corruption. */
export function parseProjectFile(raw: unknown): ProjectFile {
const obj = raw as Partial<ProjectFile>;
@@ -22,48 +99,16 @@ export function parseProjectFile(raw: unknown): ProjectFile {
throw new Error('Not a valid Hydro Builder project file.');
}
const VALID_CATEGORIES = new Set<string>([
'Plumbing',
'Flow Control',
'Growing',
'Structure',
'Environment',
]);
const validatedParts: PlacedPart[] = [];
for (const p of obj.parts) {
if (!p || typeof p !== 'object') {
throw new Error('Project contains invalid or null part data.');
}
if (typeof p.id !== 'string' || !p.id) {
throw new Error('Part is missing a valid ID.');
}
if (typeof p.type !== 'string' || !(p.type in PART_DEFS)) {
throw new Error(`Part ${p.id} has an invalid or unrecognized type: ${p.type}.`);
}
const category = PART_DEFS[p.type as PartType].category;
if (!VALID_CATEGORIES.has(category)) {
throw new Error(`Part ${p.id} has an invalid category: ${category}.`);
}
if (
!Array.isArray(p.position) ||
p.position.length !== 3 ||
!p.position.every((num) => typeof num === 'number' && Number.isFinite(num))
) {
throw new Error(`Part ${p.id} must have a position with an array of 3 numbers.`);
}
if (
!Array.isArray(p.rotation) ||
p.rotation.length !== 3 ||
!p.rotation.every((num) => typeof num === 'number' && Number.isFinite(num))
) {
throw new Error(`Part ${p.id} must have a rotation with an array of 3 numbers.`);
try {
validatedParts.push(sanitizePart(p));
} catch (err) {
throw new Error(`Project file validation failed: ${(err as Error).message}`);
}
}
const parts = obj.parts as PlacedPart[];
const parts = validatedParts;
return toProjectFileNamed(obj.name ?? 'Imported project', parts, obj.savedAt);
}