@visulima/fs
Version:
Human friendly file system utilities for Node.js
206 lines (200 loc) • 6.38 kB
JavaScript
import { createRequire as __cjs_createRequire } from "node:module";
const __cjs_require = __cjs_createRequire(import.meta.url);
const __cjs_getProcess = typeof globalThis !== "undefined" && typeof globalThis.process !== "undefined" ? globalThis.process : process;
const {
platform
} = __cjs_getProcess;
const FALLBACK_NAME = "unnamed";
const MAX_LENGTH = 128;
const REPLACEMENT_CHARACTERS = {
'"': "ˮ",
// 0x02EE - MODIFIER LETTER DOUBLE APOSTROPHE
"*": "⁎",
// 0x204E - LOW ASTERISK
"/": "⁄",
// 0x2044 - FRACTION SLASH
":": "꞉",
// 0xA789 - MODIFIER LETTER COLON
"<": "‹",
// 0x2039 - SINGLE LEFT-POINTING ANGLE QUOTATION MARK
">": "›",
// 0x203A - SINGLE RIGHT-POINTING ANGLE QUOTATION MARK
"?": "ʔ",
// 0x0294 - LATIN LETTER GLOTTAL STOP
"\\": "∖",
// 0x2216 - SET MINUS
"|": "ǀ"
// 0x01C0 - LATIN LETTER DENTAL CLICK
};
const CONTROL_CHARS_REGEX = /[\u0000-\u001F\u0080-\u009F]/g;
const RELATIVE_PATHS_REGEX = /^(?:\.+[/\\]+)+|^\.+$/g;
const WINDOWS_SPECIAL_NAMES_REGEX = /^(?:con|prn|aux|nul|com\d|lpt\d)$/i;
const WINDOWS_FORBIDDEN_CHARS_REGEX = /[<>:"/\\|?*]/g;
const UNIX_FORBIDDEN_CHARS_REGEX = /\//g;
const TRIM_CHARS = /* @__PURE__ */ new Set([" ", " ", "."]);
const hasExtension = (name, lastDotIndex) => lastDotIndex > 0 && lastDotIndex < name.length - 1;
const truncateWithExtension = (name, maxLength) => {
const lastDotIndex = name.lastIndexOf(".");
if (!hasExtension(name, lastDotIndex)) {
return name.slice(0, maxLength);
}
const baseName = name.slice(0, lastDotIndex);
const extension = name.slice(lastDotIndex);
const maxBaseLength = maxLength - extension.length;
return maxBaseLength > 0 ? `${baseName.slice(0, maxBaseLength)}${extension}` : name.slice(0, maxLength);
};
const trimSpacesAndPeriods = (input) => {
let start = 0;
let end = input.length;
while (start < end) {
const char = input[start];
if (char === void 0 || !TRIM_CHARS.has(char)) {
break;
}
start += 1;
}
while (end > start) {
const char = input[end - 1];
if (char === void 0 || !TRIM_CHARS.has(char)) {
break;
}
end -= 1;
}
return input.slice(start, end);
};
const cleanWindowsName = (name) => {
const lastDotIndex = name.lastIndexOf(".");
if (hasExtension(name, lastDotIndex)) {
const baseName = name.slice(0, lastDotIndex);
const extension = name.slice(lastDotIndex);
let cleanedBaseName = baseName;
while (cleanedBaseName.length > 0) {
const char = cleanedBaseName[cleanedBaseName.length - 1];
if (char === void 0 || !TRIM_CHARS.has(char)) {
break;
}
cleanedBaseName = cleanedBaseName.slice(0, -1);
}
if (cleanedBaseName.length === 0) {
return extension.slice(1);
}
return `${cleanedBaseName}${extension}`;
}
let cleaned = name;
while (cleaned.length > 0) {
const char = cleaned[cleaned.length - 1];
if (char === void 0 || !TRIM_CHARS.has(char)) {
break;
}
cleaned = cleaned.slice(0, -1);
}
return cleaned;
};
const isWindowsReservedName = (name) => {
const lastDotIndex = name.lastIndexOf(".");
if (hasExtension(name, lastDotIndex)) {
return false;
}
if (WINDOWS_SPECIAL_NAMES_REGEX.test(name)) {
return true;
}
let trimmed = name;
while (trimmed.length > 0) {
const char = trimmed[trimmed.length - 1];
if (char === void 0 || !TRIM_CHARS.has(char)) {
break;
}
trimmed = trimmed.slice(0, -1);
}
if (trimmed.length > 0 && WINDOWS_SPECIAL_NAMES_REGEX.test(trimmed)) {
return true;
}
return false;
};
const cleanFat32Name = (name) => {
const lastDotIndex = name.lastIndexOf(".");
if (hasExtension(name, lastDotIndex)) {
const baseName = name.slice(0, lastDotIndex);
const extension = name.slice(lastDotIndex);
const cleanedBaseName = trimSpacesAndPeriods(baseName);
if (cleanedBaseName.length === 0) {
return void 0;
}
return `${cleanedBaseName}${extension}`;
}
const trimmed = name.trim();
if (trimmed.length > 0 && trimmed[0] === "." && trimmed.length <= 5) {
const secondDotIndex = trimmed.indexOf(".", 1);
if (secondDotIndex === -1) {
return void 0;
}
}
const cleaned = trimSpacesAndPeriods(name);
if (cleaned.length === 0) {
return void 0;
}
return cleaned;
};
const getFileSystemType = (options) => {
const filesystem = options?.filesystem ?? "auto";
if (filesystem === "auto") {
return platform === "win32" ? "win32" : "unix";
}
if (filesystem === "darwin") {
return "unix";
}
return filesystem;
};
const sanitize = (name, options) => {
if (typeof name !== "string" || name.length === 0) {
return FALLBACK_NAME;
}
const fileSystemType = getFileSystemType(options);
let sanitized = name;
sanitized = sanitized.replace(CONTROL_CHARS_REGEX, "");
if (sanitized.length === 0) {
return FALLBACK_NAME;
}
sanitized = sanitized.replace(RELATIVE_PATHS_REGEX, "");
if (sanitized.length === 0) {
return FALLBACK_NAME;
}
if (fileSystemType === "win32" || fileSystemType === "fat32") {
const lastDotIndex = sanitized.lastIndexOf(".");
if (!hasExtension(sanitized, lastDotIndex) && WINDOWS_SPECIAL_NAMES_REGEX.test(sanitized)) {
return FALLBACK_NAME;
}
}
sanitized = fileSystemType === "win32" || fileSystemType === "fat32" ? sanitized.replaceAll(WINDOWS_FORBIDDEN_CHARS_REGEX, (char) => REPLACEMENT_CHARACTERS[char] ?? "") : sanitized.replaceAll(UNIX_FORBIDDEN_CHARS_REGEX, REPLACEMENT_CHARACTERS["/"]);
sanitized = sanitized.trim();
if (fileSystemType === "win32" || fileSystemType === "fat32") {
if (isWindowsReservedName(sanitized)) {
return FALLBACK_NAME;
}
if (fileSystemType === "win32") {
sanitized = cleanWindowsName(sanitized);
if (sanitized.length === 0) {
return FALLBACK_NAME;
}
}
}
if (fileSystemType === "fat32") {
const cleaned = cleanFat32Name(sanitized);
if (cleaned === void 0) {
return FALLBACK_NAME;
}
sanitized = cleaned;
}
if (sanitized.length === 0) {
return FALLBACK_NAME;
}
const maxLength = options?.maxLength ?? MAX_LENGTH;
if (sanitized.length > maxLength) {
sanitized = truncateWithExtension(sanitized, maxLength);
if (sanitized.length === 0) {
return FALLBACK_NAME;
}
}
return sanitized;
};
export { sanitize };