From 9dc0232e4a88cfac87731e32ade096a87e1459f6 Mon Sep 17 00:00:00 2001 From: Tom Boullay Date: Tue, 28 Apr 2026 00:17:28 +0200 Subject: [PATCH] fix: validate texture asset names server-side --- lib/asset-classification.ts | 12 +++++++++-- lib/asset-naming.ts | 38 +++++++++++++++++++++++++++++++++ lib/commit-message.ts | 4 +++- lib/parse-upload.ts | 24 ++++++++++++++++----- lib/validate-folder.ts | 42 ++----------------------------------- 5 files changed, 72 insertions(+), 48 deletions(-) diff --git a/lib/asset-classification.ts b/lib/asset-classification.ts index 162491d..b052575 100644 --- a/lib/asset-classification.ts +++ b/lib/asset-classification.ts @@ -1,15 +1,19 @@ import { getAssetFamily } from './asset-naming' -export type AssetCategory = 'color' | 'roughness' | 'normal' | 'metalness' | 'assets' +export type AssetCategory = 'color' | 'diffuse' | 'roughness' | 'normal' | 'metalness' | 'opacity' | 'assets' export function classifyAssetCategory(filename: string): AssetCategory { const name = filename.replace(/\.[^.]+$/, '') const family = getAssetFamily(name.split('_')[0]) - if (family === 'color' || family === 'diffuse') { + if (family === 'color') { return 'color' } + if (family === 'diffuse') { + return 'diffuse' + } + if (family === 'roughness') { return 'roughness' } @@ -22,5 +26,9 @@ export function classifyAssetCategory(filename: string): AssetCategory { return 'metalness' } + if (family === 'opacity') { + return 'opacity' + } + return 'assets' } diff --git a/lib/asset-naming.ts b/lib/asset-naming.ts index c0ff7aa..56a6446 100644 --- a/lib/asset-naming.ts +++ b/lib/asset-naming.ts @@ -29,6 +29,44 @@ export function getForbiddenAssetFamilyAlias(value: string): AssetFamily | undef return FORBIDDEN_ASSET_FAMILY_ALIASES.get(value.toLowerCase()) } +function getFileStem(filename: string) { + return filename.replace(/\.[^.]+$/, '') +} + +export function getTextureNamingError(filename: string) { + const stem = getFileStem(filename) + const [prefix, ...targetParts] = stem.split('_') + const family = getAssetFamily(prefix) + const extension = filename.split('.').pop() + + if (family && targetParts.every(Boolean)) return null + + const aliasSuggestion = getForbiddenAssetFamilyAlias(prefix) + + if (aliasSuggestion && targetParts.every(Boolean)) { + const target = targetParts.join('_') + return `Convention invalide : ${filename}. Utilisez ${aliasSuggestion}_${target}.${extension} pour cibler "${target}", ou ${aliasSuggestion}.${extension} pour tout le modele.` + } + + const reversedParts = stem.split('_') + const reversedFamily = reversedParts.length > 1 ? getAssetFamily(reversedParts[reversedParts.length - 1]) : undefined + const reversedAliasSuggestion = reversedParts.length > 1 + ? getForbiddenAssetFamilyAlias(reversedParts[reversedParts.length - 1]) + : undefined + + if (reversedFamily) { + const target = reversedParts.slice(0, -1).join('_') + return `Convention invalide : ${filename}. Utilisez ${reversedFamily}_${target}.${extension} pour cibler "${target}", ou ${reversedFamily}.${extension} pour tout le modele.` + } + + if (reversedAliasSuggestion) { + const target = reversedParts.slice(0, -1).join('_') + return `Convention invalide : ${filename}. Utilisez ${reversedAliasSuggestion}_${target}.${extension} pour cibler "${target}", ou ${reversedAliasSuggestion}.${extension} pour tout le modele.` + } + + return `Asset inconnu : ${filename}. Familles autorisees : ${formatAssetFamilies()}. Utilisez asset.png pour tout le modele ou asset_objet.png pour cibler un objet.` +} + export function formatAssetFamilies() { return ASSET_FAMILIES.join(', ') } diff --git a/lib/commit-message.ts b/lib/commit-message.ts index d15f027..fbe5e81 100644 --- a/lib/commit-message.ts +++ b/lib/commit-message.ts @@ -58,13 +58,15 @@ export function buildCommitMessage( const sectionTitles: Record = { color: '🎨 Textures (color)', + diffuse: '🖌 Textures (diffuse)', roughness: '🪶 Textures (roughness)', normal: '🧭 Textures (normal)', metalness: '🔩 Textures (metalness)', + opacity: '🪟 Textures (opacity)', assets: '🧩 Assets', } - for (const category of ['color', 'roughness', 'normal', 'metalness', 'assets'] as const) { + for (const category of ['color', 'diffuse', 'roughness', 'normal', 'metalness', 'opacity', 'assets'] as const) { const entries = grouped.get(category) if (!entries || entries.length === 0) continue lines.push('') diff --git a/lib/parse-upload.ts b/lib/parse-upload.ts index dba1d9c..60366cf 100644 --- a/lib/parse-upload.ts +++ b/lib/parse-upload.ts @@ -1,7 +1,8 @@ import { extname } from 'path' import { NextRequest } from 'next/server' import { sanitizeFilename } from './sanitize' -import { ALL_ALLOWED_EXTENSIONS, MODEL_EXTENSIONS, MAX_FILE_SIZE } from './constants' +import { ALL_ALLOWED_EXTENSIONS, MODEL_EXTENSIONS, MAX_FILE_SIZE, TEXTURE_EXTENSIONS } from './constants' +import { getTextureNamingError } from './asset-naming' import type { ParsedFile } from './types' interface ParsedUpload { @@ -48,10 +49,10 @@ export async function parseMultiUpload(req: NextRequest): Promise const texName = textureNames[i] || '' const originalSafe = sanitizeFilename(file.name) - const ext = extname(originalSafe).toLowerCase() + const originalExt = extname(originalSafe).toLowerCase() - if (!ALL_ALLOWED_EXTENSIONS.has(ext)) { - throw new Error(`Extension non autorisee: "${ext}"`) + if (!ALL_ALLOWED_EXTENSIONS.has(originalExt)) { + throw new Error(`Extension non autorisee: "${originalExt}"`) } let filename: string @@ -61,7 +62,20 @@ export async function parseMultiUpload(req: NextRequest): Promise filename = originalSafe } - const isModel = MODEL_EXTENSIONS.has(ext) + const filenameExt = extname(filename).toLowerCase() + if (filenameExt !== originalExt) { + throw new Error(`Nom de fichier incoherent : ${filename} ne correspond pas a l'extension originale ${originalExt}`) + } + + const textureNamingError = TEXTURE_EXTENSIONS.has(filenameExt) + ? getTextureNamingError(filename) + : null + + if (textureNamingError) { + throw new Error(textureNamingError) + } + + const isModel = MODEL_EXTENSIONS.has(filenameExt) if (isModel) { if (filename.toLowerCase() !== 'model.gltf') { throw new Error('Le modele doit etre nomme model.gltf') diff --git a/lib/validate-folder.ts b/lib/validate-folder.ts index e2495f3..7c183fe 100644 --- a/lib/validate-folder.ts +++ b/lib/validate-folder.ts @@ -1,5 +1,5 @@ import { ASSET_EXTENSIONS, TEXTURE_EXTENSIONS } from '@/lib/constants' -import { formatAssetFamilies, getAssetFamily, getForbiddenAssetFamilyAlias } from '@/lib/asset-naming' +import { getTextureNamingError } from '@/lib/asset-naming' import { getErrorMessage, isRecord } from '@/lib/guards' import type { TextureFile } from '@/lib/client-types' @@ -37,44 +37,6 @@ function getFileExtension(filename: string) { return filename.slice(filename.lastIndexOf('.')).toLowerCase() } -function getFileStem(filename: string) { - return filename.replace(/\.[^.]+$/, '') -} - -function getTextureNamingError(file: File) { - const stem = getFileStem(file.name) - const [prefix, ...targetParts] = stem.split('_') - const family = getAssetFamily(prefix) - const extension = file.name.split('.').pop() - - if (family && targetParts.every(Boolean)) return null - - const aliasSuggestion = getForbiddenAssetFamilyAlias(prefix) - - if (aliasSuggestion && targetParts.every(Boolean)) { - const target = targetParts.join('_') - return `Convention invalide : ${file.name}. Utilisez ${aliasSuggestion}_${target}.${extension} pour cibler "${target}", ou ${aliasSuggestion}.${extension} pour tout le modele.` - } - - const reversedParts = stem.split('_') - const reversedFamily = reversedParts.length > 1 ? getAssetFamily(reversedParts[reversedParts.length - 1]) : undefined - const reversedAliasSuggestion = reversedParts.length > 1 - ? getForbiddenAssetFamilyAlias(reversedParts[reversedParts.length - 1]) - : undefined - - if (reversedFamily) { - const target = reversedParts.slice(0, -1).join('_') - return `Convention invalide : ${file.name}. Utilisez ${reversedFamily}_${target}.${extension} pour cibler "${target}", ou ${reversedFamily}.${extension} pour tout le modele.` - } - - if (reversedAliasSuggestion) { - const target = reversedParts.slice(0, -1).join('_') - return `Convention invalide : ${file.name}. Utilisez ${reversedAliasSuggestion}_${target}.${extension} pour cibler "${target}", ou ${reversedAliasSuggestion}.${extension} pour tout le modele.` - } - - return `Asset inconnu : ${file.name}. Familles autorisees : ${formatAssetFamilies()}. Utilisez asset.png pour tout le modele ou asset_objet.png pour cibler un objet.` -} - async function getGltfWarnings(model: File, supportFiles: File[]) { const warnings: string[] = [] let parsed: unknown @@ -132,7 +94,7 @@ export async function validateFolder(files: File[]): Promise { const textureNamingErrors = supportFiles .filter((file) => TEXTURE_EXTENSIONS.has(getFileExtension(file.name))) - .map(getTextureNamingError) + .map((file) => getTextureNamingError(file.name)) .filter((error): error is string => Boolean(error)) errors.push(...textureNamingErrors)