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
JavaScript
/**
* 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