UNPKG

fake-it-til-you-git

Version:

A modern CLI tool to generate fake Git commit history for your GitHub/GitLab profile

396 lines 15.1 kB
/** * Validation utilities for configuration and user inputs */ import { isValidDate, parseDate } from './dates.js'; import { validateCustomMessages } from './messages.js'; /** * Validates email format using a robust regex pattern */ export function isValidEmail(email) { const trimmed = email.trim(); // Basic structure validation const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/; if (!emailRegex.test(trimmed)) { return false; } // Additional validation for consecutive dots if (trimmed.includes('..')) { return false; } return true; } /** * Validates author name format */ export function isValidAuthorName(name) { const trimmed = name.trim(); // Must not be empty if (trimmed.length === 0) return false; // Must be reasonable length if (trimmed.length > 100) return false; // Must not contain control characters or line breaks if (/[\x00-\x1f\x7f-\x9f]/.test(trimmed)) return false; // Must not start or end with whitespace after trimming if (trimmed !== name) return false; return true; } /** * Validates author configuration */ export function validateAuthor(author) { const errors = []; const warnings = []; if (!author || typeof author !== 'object') { errors.push('Author configuration must be an object'); return { valid: false, errors, warnings }; } const authorObj = author; // Validate name if (!authorObj.name || typeof authorObj.name !== 'string') { errors.push('Author name is required and must be a string'); } else if (!isValidAuthorName(authorObj.name)) { errors.push('Author name is invalid (must be 1-100 characters, no control characters)'); } // Validate email if (!authorObj.email || typeof authorObj.email !== 'string') { errors.push('Author email is required and must be a string'); } else if (!isValidEmail(authorObj.email)) { errors.push('Author email format is invalid'); } return { valid: errors.length === 0, errors, warnings }; } /** * Validates date range configuration */ export function validateDateRange(dateRange) { const errors = []; const warnings = []; if (!dateRange || typeof dateRange !== 'object') { errors.push('Date range configuration must be an object'); return { valid: false, errors, warnings }; } const rangeObj = dateRange; // Validate startDate if (!rangeObj.startDate || typeof rangeObj.startDate !== 'string') { errors.push('Start date is required and must be a string'); } else if (!isValidDate(rangeObj.startDate)) { errors.push('Start date format is invalid (expected YYYY-MM-DD)'); } // Validate endDate if (!rangeObj.endDate || typeof rangeObj.endDate !== 'string') { errors.push('End date is required and must be a string'); } else if (!isValidDate(rangeObj.endDate)) { errors.push('End date format is invalid (expected YYYY-MM-DD)'); } // Validate date range logic if (errors.length === 0) { const startDate = parseDate(rangeObj.startDate); const endDate = parseDate(rangeObj.endDate); if (startDate && endDate) { if (startDate > endDate) { errors.push('Start date must be before or equal to end date'); } // Check if the range is too far in the future const today = new Date(); if (endDate > today) { warnings.push('End date is in the future'); } // Check if the range is very large const daysDiff = Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)); if (daysDiff > 3650) { // More than 10 years warnings.push('Date range spans more than 10 years, this may take a while'); } } } return { valid: errors.length === 0, errors, warnings }; } /** * Validates commits configuration */ export function validateCommits(commits) { const errors = []; const warnings = []; if (!commits || typeof commits !== 'object') { errors.push('Commits configuration must be an object'); return { valid: false, errors, warnings }; } const commitsObj = commits; // Validate maxPerDay if (commitsObj.maxPerDay === undefined || commitsObj.maxPerDay === null) { errors.push('maxPerDay is required'); } else if (typeof commitsObj.maxPerDay !== 'number' || !Number.isInteger(commitsObj.maxPerDay)) { errors.push('maxPerDay must be an integer'); } else if (commitsObj.maxPerDay < 1) { errors.push('maxPerDay must be at least 1'); } else if (commitsObj.maxPerDay > 100) { warnings.push('maxPerDay is very high (>100), this may create unrealistic commit patterns'); } // Validate distribution const validDistributions = ['uniform', 'random', 'gaussian', 'custom', 'pattern']; if (!commitsObj.distribution || typeof commitsObj.distribution !== 'string') { errors.push('Distribution is required and must be a string'); } else if (!validDistributions.includes(commitsObj.distribution)) { errors.push(`Distribution must be one of: ${validDistributions.join(', ')}`); } // Validate pattern configuration if distribution is pattern if (commitsObj.distribution === 'pattern') { if (!commitsObj.pattern || typeof commitsObj.pattern !== 'object') { errors.push('Pattern configuration is required when using pattern distribution'); } else { const patternValidation = validatePattern(commitsObj.pattern); errors.push(...patternValidation.errors); warnings.push(...patternValidation.warnings); } } // Validate messageStyle const validMessageStyles = ['default', 'lorem', 'emoji']; if (!commitsObj.messageStyle || typeof commitsObj.messageStyle !== 'string') { errors.push('Message style is required and must be a string'); } else if (!validMessageStyles.includes(commitsObj.messageStyle)) { errors.push(`Message style must be one of: ${validMessageStyles.join(', ')}`); } // Validate customMessages if present if (commitsObj.customMessages !== undefined) { if (!Array.isArray(commitsObj.customMessages)) { errors.push('Custom messages must be an array'); } else { const messageValidation = validateCustomMessages(commitsObj.customMessages); if (!messageValidation.valid) { errors.push(...messageValidation.errors.map((err) => `Custom messages: ${err}`)); } } } return { valid: errors.length === 0, errors, warnings }; } /** * Validates pattern configuration */ export function validatePattern(pattern) { const errors = []; const warnings = []; if (!pattern || typeof pattern !== 'object') { errors.push('Pattern configuration must be an object'); return { valid: false, errors, warnings }; } const patternObj = pattern; // Validate type const validTypes = ['preset', 'custom', 'text']; if (patternObj.type && typeof patternObj.type === 'string') { if (!validTypes.includes(patternObj.type)) { errors.push(`Pattern type must be one of: ${validTypes.join(', ')}`); } } // Validate preset if (patternObj.type === 'preset') { const validPresets = ['heart', 'star', 'wave', 'text', 'square', 'triangle', 'diamond', 'cross']; if (!patternObj.preset || typeof patternObj.preset !== 'string') { errors.push('Preset name is required when type is preset'); } else if (!validPresets.includes(patternObj.preset)) { errors.push(`Preset must be one of: ${validPresets.join(', ')}`); } } // Validate custom pattern if (patternObj.type === 'custom') { if (!patternObj.custom || typeof patternObj.custom !== 'string') { errors.push('Custom pattern string is required when type is custom'); } else if (patternObj.custom.length > 10000) { warnings.push('Custom pattern is very large and may impact performance'); } } // Validate text pattern if (patternObj.type === 'text') { if (!patternObj.text || typeof patternObj.text !== 'string') { errors.push('Text is required when type is text'); } else if (patternObj.text.length > 20) { warnings.push('Text pattern is very long and may not fit well in the contribution graph'); } } // Validate scale if (patternObj.scale !== undefined) { if (typeof patternObj.scale !== 'number' || !Number.isInteger(patternObj.scale)) { errors.push('Scale must be an integer'); } else if (patternObj.scale < 1 || patternObj.scale > 5) { errors.push('Scale must be between 1 and 5'); } } // Validate intensity const validIntensities = ['low', 'medium', 'high']; if (patternObj.intensity !== undefined) { if (typeof patternObj.intensity !== 'string') { errors.push('Intensity must be a string'); } else if (!validIntensities.includes(patternObj.intensity)) { errors.push(`Intensity must be one of: ${validIntensities.join(', ')}`); } } // Validate centerX and centerY if (patternObj.centerX !== undefined) { if (typeof patternObj.centerX !== 'number') { errors.push('CenterX must be a number'); } else if (patternObj.centerX < 0 || patternObj.centerX > 1) { errors.push('CenterX must be between 0 and 1'); } } if (patternObj.centerY !== undefined) { if (typeof patternObj.centerY !== 'number') { errors.push('CenterY must be a number'); } else if (patternObj.centerY < 0 || patternObj.centerY > 1) { errors.push('CenterY must be between 0 and 1'); } } return { valid: errors.length === 0, errors, warnings, }; } /** * Validates options configuration */ export function validateOptions(options) { const errors = []; const warnings = []; // Options are optional, so undefined/null is valid if (!options) { return { valid: true, errors, warnings }; } if (typeof options !== 'object') { errors.push('Options configuration must be an object'); return { valid: false, errors, warnings }; } const optionsObj = options; // Validate preview if (optionsObj.preview !== undefined && typeof optionsObj.preview !== 'boolean') { errors.push('preview option must be a boolean'); } // Validate dev if (optionsObj.dev !== undefined && typeof optionsObj.dev !== 'boolean') { errors.push('dev option must be a boolean'); } // Validate push if (optionsObj.push !== undefined && typeof optionsObj.push !== 'boolean') { errors.push('push option must be a boolean'); } // Validate verbose if (optionsObj.verbose !== undefined && typeof optionsObj.verbose !== 'boolean') { errors.push('verbose option must be a boolean'); } // Validate seed if (optionsObj.seed !== undefined) { if (typeof optionsObj.seed !== 'string') { errors.push('seed option must be a string'); } else if (optionsObj.seed.trim().length === 0) { errors.push('seed option cannot be empty'); } } return { valid: errors.length === 0, errors, warnings }; } /** * Validates the complete configuration object */ export function validateConfig(config) { const errors = []; const warnings = []; if (!config || typeof config !== 'object') { errors.push('Configuration must be an object'); return { valid: false, errors, warnings }; } const configObj = config; // Validate each section const authorValidation = validateAuthor(configObj.author); const dateRangeValidation = validateDateRange(configObj.dateRange); const commitsValidation = validateCommits(configObj.commits); const optionsValidation = validateOptions(configObj.options); // Collect all errors and warnings errors.push(...authorValidation.errors); errors.push(...dateRangeValidation.errors); errors.push(...commitsValidation.errors); errors.push(...optionsValidation.errors); warnings.push(...authorValidation.warnings); warnings.push(...dateRangeValidation.warnings); warnings.push(...commitsValidation.warnings); warnings.push(...optionsValidation.warnings); return { valid: errors.length === 0, errors, warnings }; } /** * Sanitizes a string by trimming whitespace and removing control characters */ export function sanitizeString(input) { return input .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/g, '') // Remove control characters but keep \t (\x09) and \n (\x0a) .replace(/\s+/g, ' ') // Normalize whitespace (including tabs and newlines to spaces) .trim(); // Trim after normalization } /** * Sanitizes an email address */ export function sanitizeEmail(email) { return sanitizeString(email).toLowerCase(); } /** * Sanitizes an author name */ export function sanitizeAuthorName(name) { const sanitized = sanitizeString(name); // Ensure it doesn't exceed max length return sanitized.length > 100 ? sanitized.substring(0, 100).trim() : sanitized; } /** * Sanitizes the entire configuration object */ export function sanitizeConfig(config) { const sanitized = { author: { name: sanitizeAuthorName(config.author.name), email: sanitizeEmail(config.author.email), }, dateRange: { startDate: sanitizeString(config.dateRange.startDate), endDate: sanitizeString(config.dateRange.endDate), }, commits: { maxPerDay: Math.max(1, Math.floor(Math.abs(config.commits.maxPerDay))), distribution: config.commits.distribution, messageStyle: config.commits.messageStyle, ...(config.commits.customMessages && { customMessages: config.commits.customMessages .map((msg) => sanitizeString(msg)) .filter((msg) => msg.length > 0), }), }, options: config.options ? { preview: Boolean(config.options.preview), push: Boolean(config.options.push), verbose: Boolean(config.options.verbose), dev: Boolean(config.options.dev), ...(config.options.seed && { seed: sanitizeString(config.options.seed) }), } : {}, }; return sanitized; } //# sourceMappingURL=validation.js.map