Harden import and paste validation, fix duplicate exports and variable declarations
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user