UNPKG

@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
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 };