/** * User Input Validation and Formatting Library * * Implements the rules defined in docs/input_validation_plan.md. * Provides formatting functions and a Zod-integrated pipeline that: * 1. Removes disallowed characters * 2. Trims leading/trailing whitespace * 3. Validates format (email) * 4. Applies case transformation * 5. Truncates to max length */ // ─── Character class definitions ─────────────────────────────── const LETTERS_NUMBERS = 'a-zA-Z0-9' const LETTERS_NUMBERS_HYPHEN = `${LETTERS_NUMBERS}\\-` const LETTERS_SPACES_HYPHEN = 'a-zA-Z\\s\\-' const BASE_TEXT = `${LETTERS_NUMBERS}\\s\\-\\/` const EXTENDED_TEXT = `${BASE_TEXT},` // ─── Field type definitions ──────────────────────────────────── export type FieldType = | 'name' | 'nationality' | 'streetAddress' | 'city' | 'pickupLocation' | 'returnLocation' | 'country' | 'fullAddress' | 'commercialName' | 'legalCompanyName' | 'email' | 'licensePlate' | 'vin' | 'cin' | 'passportNumber' | 'internationalPermitNumber' | 'driverLicenseNumber' | 'licenseCategory' | 'iceNumber' | 'commercialRegistry' | 'carMark' | 'carModel' | 'carColor' | 'zipCode' interface FieldConfig { maxLength: number allowedPattern: RegExp transform: (value: string) => string } // ─── Formatting functions ────────────────────────────────────── /** Uppercase first letter of each word, lowercase the rest */ export function toTitleCase(str: string): string { return str.replace(/\b\w+/g, (word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) } /** Uppercase first letter of every word */ export function toTitleCaseAll(str: string): string { return str.replace(/\b\w+/g, (word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) } export function toLowerCase(str: string): string { return str.toLowerCase() } /** Uppercase letters only, leave numbers unchanged */ export function toUpperCase(str: string): string { return str.replace(/[a-zA-Z]/g, (char) => char.toUpperCase()) } export function preserveOriginal(str: string): string { return str } // ─── Field configuration ─────────────────────────────────────── const FIELD_CONFIGS: Record = { // Group 1: Title Case name: { maxLength: 50, allowedPattern: new RegExp('^[' + LETTERS_SPACES_HYPHEN + ']*$'), transform: toTitleCase }, nationality: { maxLength: 50, allowedPattern: new RegExp('^[' + LETTERS_SPACES_HYPHEN + ']*$'), transform: toTitleCase }, streetAddress: { maxLength: 100, allowedPattern: new RegExp('^[' + BASE_TEXT + ']*$'), transform: toTitleCase }, city: { maxLength: 50, allowedPattern: new RegExp('^[' + LETTERS_SPACES_HYPHEN + ']*$'), transform: toTitleCase }, pickupLocation: { maxLength: 50, allowedPattern: new RegExp('^[' + BASE_TEXT + ']*$'), transform: toTitleCase }, returnLocation: { maxLength: 50, allowedPattern: new RegExp('^[' + BASE_TEXT + ']*$'), transform: toTitleCase }, country: { maxLength: 50, allowedPattern: new RegExp('^[' + LETTERS_SPACES_HYPHEN + ']*$'), transform: toTitleCase }, // Group 2: Title Case All fullAddress: { maxLength: 200, allowedPattern: new RegExp('^[' + EXTENDED_TEXT + ']*$'), transform: toTitleCaseAll }, commercialName: { maxLength: 100, allowedPattern: new RegExp('^[' + EXTENDED_TEXT + ']*$'), transform: toTitleCaseAll }, legalCompanyName: { maxLength: 100, allowedPattern: new RegExp('^[' + EXTENDED_TEXT + ']*$'), transform: toTitleCaseAll }, // Group 3: Lowercase — email email: { maxLength: 100, allowedPattern: new RegExp('^[' + LETTERS_NUMBERS + '@._%\\+\\-]*$'), transform: toLowerCase }, // Group 4: Uppercase (hyphens allowed per plan examples: "abc-123" → "ABC-123") licensePlate: { maxLength: 30, allowedPattern: new RegExp('^[' + LETTERS_NUMBERS_HYPHEN + ']*$'), transform: toUpperCase }, vin: { maxLength: 30, allowedPattern: new RegExp('^[' + LETTERS_NUMBERS_HYPHEN + ']*$'), transform: toUpperCase }, cin: { maxLength: 30, allowedPattern: new RegExp('^[' + LETTERS_NUMBERS_HYPHEN + ']*$'), transform: toUpperCase }, passportNumber: { maxLength: 30, allowedPattern: new RegExp('^[' + LETTERS_NUMBERS_HYPHEN + ']*$'), transform: toUpperCase }, internationalPermitNumber:{ maxLength: 30, allowedPattern: new RegExp('^[' + LETTERS_NUMBERS_HYPHEN + ']*$'), transform: toUpperCase }, driverLicenseNumber: { maxLength: 30, allowedPattern: new RegExp('^[' + LETTERS_NUMBERS_HYPHEN + ']*$'), transform: toUpperCase }, licenseCategory: { maxLength: 30, allowedPattern: new RegExp('^[' + LETTERS_NUMBERS_HYPHEN + ']*$'), transform: toUpperCase }, iceNumber: { maxLength: 30, allowedPattern: new RegExp('^[' + LETTERS_NUMBERS_HYPHEN + ']*$'), transform: toUpperCase }, commercialRegistry: { maxLength: 30, allowedPattern: new RegExp('^[' + LETTERS_NUMBERS_HYPHEN + ']*$'), transform: toUpperCase }, // Group 5: Car fields carMark: { maxLength: 30, allowedPattern: new RegExp('^[' + LETTERS_NUMBERS + '\\s\\-]*$'), transform: toTitleCase }, carModel: { maxLength: 30, allowedPattern: new RegExp('^[' + LETTERS_NUMBERS + '\\s\\-]*$'), transform: toTitleCase }, carColor: { maxLength: 30, allowedPattern: new RegExp('^[' + LETTERS_NUMBERS + '\\s\\-]*$'), transform: toTitleCase }, // Group 6: Preserved (spaces for UK postcodes like "SW1A 1AA") zipCode: { maxLength: 10, allowedPattern: new RegExp('^[' + LETTERS_NUMBERS + '\\s\\-]*$'), transform: preserveOriginal }, } // ─── Public API ──────────────────────────────────────────────── /** Full processing pipeline: sanitize → trim → format → truncate */ export function sanitizeAndFormat(value: string, fieldType: FieldType): string { const config = FIELD_CONFIGS[fieldType] // Step 1: Remove disallowed characters let sanitized = '' for (const char of value) { if (config.allowedPattern.test(char)) { sanitized += char } } // Step 2: Trim sanitized = sanitized.trim() // Step 3: Apply case transformation const formatted = config.transform(sanitized) // Step 4: Truncate if (formatted.length > config.maxLength) { return formatted.slice(0, config.maxLength) } return formatted } /** Get the max length for a given field type */ export function getMaxLength(fieldType: FieldType): number { return FIELD_CONFIGS[fieldType].maxLength }