|
| 1 | +// Constants definition |
| 2 | +/* eslint-disable no-control-regex */ |
| 3 | +const INVALID_CHARS_REGEX = /[<>:"/\\|?*\u0000-\u001F\u007F-\u009F]/g; |
| 4 | +const ZERO_WIDTH_CHARS_REGEX = /[\u200B-\u200D\u2060\uFEFF]/g; |
| 5 | +const UNICODE_SPACES_REGEX = /[\u00A0\u2000-\u200A]/g; |
| 6 | +const LEADING_TRAILING_DOTS_SPACES_REGEX = /^[\s.]+|[\s.]+$/g; |
| 7 | +/* eslint-enable no-control-regex */ |
| 8 | + |
| 9 | +const WINDOWS_RESERVED_NAMES = new Set([ |
| 10 | + 'CON', |
| 11 | + 'PRN', |
| 12 | + 'AUX', |
| 13 | + 'NUL', |
| 14 | + 'COM1', |
| 15 | + 'COM2', |
| 16 | + 'COM3', |
| 17 | + 'COM4', |
| 18 | + 'COM5', |
| 19 | + 'COM6', |
| 20 | + 'COM7', |
| 21 | + 'COM8', |
| 22 | + 'COM9', |
| 23 | + 'LPT1', |
| 24 | + 'LPT2', |
| 25 | + 'LPT3', |
| 26 | + 'LPT4', |
| 27 | + 'LPT5', |
| 28 | + 'LPT6', |
| 29 | + 'LPT7', |
| 30 | + 'LPT8', |
| 31 | + 'LPT9', |
| 32 | +]); |
| 33 | + |
| 34 | +const DEFAULT_FALLBACK_NAME = 'untitled'; |
| 35 | +const MAX_FILENAME_LENGTH = 200; |
| 36 | + |
| 37 | +/** |
| 38 | + * Sanitizes a filename to be compatible with Mac, Linux, and Windows file systems |
| 39 | + * |
| 40 | + * Main features: |
| 41 | + * - Replace invalid characters (e.g. ":" in hello:world) |
| 42 | + * - Handle Windows reserved names |
| 43 | + * - Limit filename length |
| 44 | + * - Normalize Unicode characters |
| 45 | + * |
| 46 | + * @param filename - The filename to sanitize (without extension) |
| 47 | + * @param maxLength - Maximum filename length (default: 200) |
| 48 | + * @returns A sanitized filename (without extension) |
| 49 | + * |
| 50 | + * @example |
| 51 | + * sanitizeFilename('hello:world') // returns 'hello_world' |
| 52 | + * sanitizeFilename('CON') // returns '_CON' |
| 53 | + * sanitizeFilename('') // returns 'untitled' |
| 54 | + */ |
| 55 | +export const sanitizeFilename = ( |
| 56 | + filename: string, |
| 57 | + maxLength: number = MAX_FILENAME_LENGTH, |
| 58 | +): string => { |
| 59 | + // Input validation |
| 60 | + if (!filename) { |
| 61 | + return DEFAULT_FALLBACK_NAME; |
| 62 | + } |
| 63 | + |
| 64 | + let baseName = filename |
| 65 | + .trim() |
| 66 | + .replace(INVALID_CHARS_REGEX, '_') |
| 67 | + .replace(ZERO_WIDTH_CHARS_REGEX, '') |
| 68 | + .replace(UNICODE_SPACES_REGEX, ' ') |
| 69 | + .replace(LEADING_TRAILING_DOTS_SPACES_REGEX, ''); |
| 70 | + |
| 71 | + // Handle empty or invalid filenames after cleaning |
| 72 | + if (!baseName) { |
| 73 | + baseName = DEFAULT_FALLBACK_NAME; |
| 74 | + } |
| 75 | + |
| 76 | + // Handle Windows reserved names |
| 77 | + if (WINDOWS_RESERVED_NAMES.has(baseName.toUpperCase())) { |
| 78 | + baseName = `_${baseName}`; |
| 79 | + } |
| 80 | + |
| 81 | + // Truncate if too long |
| 82 | + if (baseName.length > maxLength) { |
| 83 | + baseName = baseName.slice(0, maxLength); |
| 84 | + } |
| 85 | + |
| 86 | + return baseName; |
| 87 | +}; |
0 commit comments