@princevish/font-converter-cli
Version:
A powerful CLI tool to convert WOFF and WOFF2 fonts to TTF format with recursive directory scanning support
259 lines (226 loc) • 7.83 kB
JavaScript
const fs = require('fs').promises;
const path = require('path');
/**
* Validate if a file exists and is readable
* @param {string} filePath - Path to the file
* @throws {Error} If file doesn't exist or isn't readable
*/
async function validateFileExists(filePath) {
try {
await fs.access(filePath, fs.constants.F_OK | fs.constants.R_OK);
} catch (error) {
if (error.code === 'ENOENT') {
throw new Error(`File not found: ${filePath}`);
} else if (error.code === 'EACCES') {
throw new Error(`Permission denied: ${filePath}`);
} else {
throw new Error(`Cannot access file: ${filePath} (${error.message})`);
}
}
}
/**
* Validate if a directory exists and is readable
* @param {string} dirPath - Path to the directory
* @throws {Error} If directory doesn't exist or isn't readable
*/
async function validateDirectoryExists(dirPath) {
try {
const stats = await fs.stat(dirPath);
if (!stats.isDirectory()) {
throw new Error(`Path is not a directory: ${dirPath}`);
}
await fs.access(dirPath, fs.constants.R_OK);
} catch (error) {
if (error.code === 'ENOENT') {
throw new Error(`Directory not found: ${dirPath}`);
} else if (error.code === 'EACCES') {
throw new Error(`Permission denied: ${dirPath}`);
} else {
throw new Error(`Cannot access directory: ${dirPath} (${error.message})`);
}
}
}
/**
* Validate font file format
* @param {string} filePath - Path to the font file
* @param {Buffer} buffer - Font file buffer
* @returns {string} Validated format
* @throws {Error} If format is invalid or unsupported
*/
function validateFontFormat(filePath, buffer) {
const ext = path.extname(filePath).toLowerCase();
const supportedExtensions = ['.woff', '.woff2'];
if (!supportedExtensions.includes(ext)) {
throw new Error(`Unsupported file extension: ${ext}. Supported formats: ${supportedExtensions.join(', ')}`);
}
if (buffer.length < 4) {
throw new Error('File is too small to be a valid font file');
}
const signature = buffer.readUInt32BE(0);
// Validate WOFF signature
if (ext === '.woff' && signature !== 0x774F4646) {
throw new Error('Invalid WOFF file: incorrect signature');
}
// Validate WOFF2 signature
if (ext === '.woff2' && signature !== 0x774F4632) {
throw new Error('Invalid WOFF2 file: incorrect signature');
}
return ext.substring(1); // Return format without dot
}
/**
* Validate output path and create directory if needed
* @param {string} outputPath - Path to the output file
* @throws {Error} If output path is invalid
*/
async function validateOutputPath(outputPath) {
const outputDir = path.dirname(outputPath);
const outputExt = path.extname(outputPath).toLowerCase();
if (outputExt !== '.ttf') {
throw new Error('Output file must have .ttf extension');
}
try {
// Try to create the directory if it doesn't exist
await fs.mkdir(outputDir, { recursive: true });
// Check if we can write to the directory
await fs.access(outputDir, fs.constants.W_OK);
} catch (error) {
if (error.code === 'EACCES') {
throw new Error(`Permission denied: cannot write to ${outputDir}`);
} else {
throw new Error(`Cannot create output directory: ${outputDir} (${error.message})`);
}
}
// Check if output file already exists and warn user
try {
await fs.access(outputPath);
// File exists, we'll overwrite it (this is expected behavior)
} catch (error) {
// File doesn't exist, which is fine
}
}
/**
* Validate file size constraints
* @param {Buffer} buffer - File buffer
* @param {string} type - File type for error messages
* @throws {Error} If file is too large or too small
*/
function validateFileSize(buffer, type = 'font') {
const maxSize = 50 * 1024 * 1024; // 50MB max
const minSize = 100; // 100 bytes min
if (buffer.length > maxSize) {
throw new Error(`${type} file is too large: ${buffer.length} bytes (max: ${maxSize} bytes)`);
}
if (buffer.length < minSize) {
throw new Error(`${type} file is too small: ${buffer.length} bytes (min: ${minSize} bytes)`);
}
}
/**
* Sanitize file path to prevent directory traversal attacks
* @param {string} filePath - Input file path
* @returns {string} Sanitized file path
* @throws {Error} If path contains dangerous patterns
*/
function sanitizePath(filePath) {
// Resolve to absolute path
const resolved = path.resolve(filePath);
// Check for dangerous patterns
const dangerous = ['..', '~', '$'];
for (const pattern of dangerous) {
if (filePath.includes(pattern)) {
throw new Error(`Potentially dangerous path pattern detected: ${pattern}`);
}
}
return resolved;
}
/**
* Comprehensive input validation for font conversion
* @param {string} inputPath - Input file path
* @param {string} outputPath - Output file path
* @returns {Object} Validation results
*/
async function validateConversionInputs(inputPath, outputPath) {
try {
// Sanitize paths
const safeInputPath = sanitizePath(inputPath);
const safeOutputPath = sanitizePath(outputPath);
// Validate input file exists
await validateFileExists(safeInputPath);
// Read and validate input file
const inputBuffer = await fs.readFile(safeInputPath);
validateFileSize(inputBuffer, 'input');
// Validate font format
const format = validateFontFormat(safeInputPath, inputBuffer);
// Validate output path
await validateOutputPath(safeOutputPath);
return {
inputPath: safeInputPath,
outputPath: safeOutputPath,
format,
inputBuffer,
inputSize: inputBuffer.length
};
} catch (error) {
throw new Error(`Validation failed: ${error.message}`);
}
}
/**
* Validate batch conversion inputs
* @param {string} inputDir - Input directory path
* @param {string} outputDir - Output directory path
* @returns {Object} Validation results
*/
async function validateBatchInputs(inputDir, outputDir) {
try {
// Sanitize paths
const safeInputDir = sanitizePath(inputDir);
const safeOutputDir = sanitizePath(outputDir);
// Validate input directory
await validateDirectoryExists(safeInputDir);
// Validate/create output directory
await fs.mkdir(safeOutputDir, { recursive: true });
await fs.access(safeOutputDir, fs.constants.W_OK);
return {
inputDir: safeInputDir,
outputDir: safeOutputDir
};
} catch (error) {
throw new Error(`Batch validation failed: ${error.message}`);
}
}
/**
* Error types for better error handling
*/
const ErrorTypes = {
FILE_NOT_FOUND: 'FILE_NOT_FOUND',
PERMISSION_DENIED: 'PERMISSION_DENIED',
INVALID_FORMAT: 'INVALID_FORMAT',
FILE_TOO_LARGE: 'FILE_TOO_LARGE',
FILE_TOO_SMALL: 'FILE_TOO_SMALL',
CONVERSION_FAILED: 'CONVERSION_FAILED',
VALIDATION_FAILED: 'VALIDATION_FAILED'
};
/**
* Create a structured error with type and context
* @param {string} type - Error type from ErrorTypes
* @param {string} message - Error message
* @param {Object} context - Additional context
* @returns {Error} Structured error
*/
function createError(type, message, context = {}) {
const error = new Error(message);
error.type = type;
error.context = context;
return error;
}
module.exports = {
validateFileExists,
validateDirectoryExists,
validateFontFormat,
validateOutputPath,
validateFileSize,
sanitizePath,
validateConversionInputs,
validateBatchInputs,
ErrorTypes,
createError
};