diff --git a/packages/fs/dist/file.js b/packages/fs/dist/file.js index 6f257ff5..f2629c6a 100644 --- a/packages/fs/dist/file.js +++ b/packages/fs/dist/file.js @@ -118,7 +118,6 @@ const checkContentAsync = (path, mode, options) => { const writeAsync = async (path, stat, options) => { const mode = normalizeFileMode(stat.mode); return checkContentAsync(path, mode, options); - //.then() }; const touchAsync = (path, options) => { return writeASync(path, options.content !== undefined ? options.content : '', { diff --git a/packages/fs/dist/index.d.ts b/packages/fs/dist/index.d.ts index 340d207e..cb0ff5c3 100644 --- a/packages/fs/dist/index.d.ts +++ b/packages/fs/dist/index.d.ts @@ -1,61 +1 @@ -import { Options as AppendOptions } from './append.js'; -import { IOptions as DirOptions } from './dir.js'; -import { IOptions as FileOptions } from './file.js'; -import { IOptions as FindOptions } from './find.js'; -import { Options as InspectTreeOptions } from './inspect_tree.js'; -import { IWriteOptions } from './interfaces.js'; -import { ICopyOptions, INode, IInspectOptions } from './interfaces.js'; -import { ReadWriteDataType, TCopyResult, ENodeType, TDeleteResult } from './interfaces.js'; -export interface IJetpack { - cwd(w?: any): IJetpack | string; - path(): string; - append(path: string, data: string | Buffer | object, options?: AppendOptions): void; - appendAsync(path: string, data: string | Buffer | object, options?: AppendOptions): Promise; - copy(from: string, to: string, options?: ICopyOptions): void; - copyAsync(from: string, to: string, options?: ICopyOptions): Promise; - createWriteStream(path: string, options?: { - flags?: string; - encoding?: string; - fd?: number; - mode?: number; - autoClose?: boolean; - start?: number; - }): any; - createReadStream(path: string, options?: { - flags?: string; - encoding?: string; - fd?: number; - mode?: number; - autoClose?: boolean; - start?: number; - end?: number; - }): any; - dir(path: string, criteria?: DirOptions): IJetpack; - dirAsync(path: string, criteria?: DirOptions): Promise; - exists(path: string): boolean | string; - existsAsync(path: string): Promise; - file(path: string, criteria?: FileOptions): void; - fileAsync(path: string, criteria?: FileOptions): Promise; - find(startPath: string, options: FindOptions): string[]; - findAsync(startPath: string, options: FindOptions): Promise; - inspect(path: string, fieldsToInclude: IInspectOptions): INode; - inspectAsync(path: string, fieldsToInclude: IInspectOptions): Promise; - inspectTree(path: string, options?: InspectTreeOptions): INode; - inspectTreeAsync(path: string, options?: InspectTreeOptions): Promise; - list(path: string): string[]; - listAsync(path: string): Promise; - move(from: string, to: string): void; - moveAsync(from: string, to: string): Promise; - read(path: string, returnAs?: string): ReadWriteDataType; - readAsync(path: string, returnAs?: string): Promise; - remove(path: string): void; - removeAsync(path: string): Promise; - rename(path: string, newName: string): void; - renameAsync(path: string, newName: string): Promise; - symlink(symlinkValue: string, path: string): void; - symlinkAsync(symlinkValue: string, path: string): Promise; - write(path: string, data: string | Buffer | object, options?: IWriteOptions): void; - writeAsync(path: string, data: string | Buffer | object, options?: IWriteOptions): Promise; -} -export declare const fs: (cwdPath?: string) => IJetpack; -export default fs; +export {}; diff --git a/packages/fs/dist/index.js b/packages/fs/dist/index.js index a6642301..cb0ff5c3 100644 --- a/packages/fs/dist/index.js +++ b/packages/fs/dist/index.js @@ -1,47 +1 @@ -import * as util from 'util'; -import * as pathUtil from 'path'; -// The Jetpack Context object. -// It provides the public API, and resolves all paths regarding to -// passed cwdPath, or default process.cwd() if cwdPath was not specified. -export const fs = (cwdPath) => { - const getCwdPath = function () { - return cwdPath || process.cwd(); - }; - const cwd = function (w) { - let args; - let pathParts; - // return current CWD if no arguments specified... - if (arguments.length === 0) { - return getCwdPath(); - } - // ...create new CWD context otherwise - args = Array.prototype.slice.call(arguments); - pathParts = [getCwdPath()].concat(args); - const res = fs(pathUtil.resolve.apply(null, pathParts)); - return res; - }; - // resolves path to inner CWD path of this jetpack instance - const resolvePath = function (path) { - return pathUtil.resolve(getCwdPath(), path); - }; - const getPath = function () { - // add CWD base path as first element of arguments array - Array.prototype.unshift.call(arguments, getCwdPath()); - return pathUtil.resolve.apply(null, arguments); - }; - const normalizeOptions = function (options) { - return options || { cwd: getCwdPath() }; - }; - // API - const api = {}; - if (util.inspect['custom'] !== undefined) { - // Without this console.log(jetpack) throws obscure error. Details: - // https://github.com/szwacz/fs-jetpack/issues/29 - // https://nodejs.org/api/util.html#util_custom_inspection_functions_on_objects - api[util.inspect['custom']] = function () { - return getCwdPath(); - }; - } - return api; -}; -export default fs; +export {}; diff --git a/packages/fs/dist/utils/index.d.ts b/packages/fs/dist/utils/index.d.ts new file mode 100644 index 00000000..e91b8609 --- /dev/null +++ b/packages/fs/dist/utils/index.d.ts @@ -0,0 +1,6 @@ +export * from "./wildcard.js"; +export * from "./paths.js"; +export * from "./name.js"; +export * from "./matcher.js"; +export * from "./mime_match.js"; +export * from "./validate.js"; diff --git a/packages/fs/dist/utils/index.js b/packages/fs/dist/utils/index.js new file mode 100644 index 00000000..e91b8609 --- /dev/null +++ b/packages/fs/dist/utils/index.js @@ -0,0 +1,6 @@ +export * from "./wildcard.js"; +export * from "./paths.js"; +export * from "./name.js"; +export * from "./matcher.js"; +export * from "./mime_match.js"; +export * from "./validate.js"; diff --git a/packages/fs/dist/utils/name.d.ts b/packages/fs/dist/utils/name.d.ts new file mode 100644 index 00000000..72d82de2 --- /dev/null +++ b/packages/fs/dist/utils/name.d.ts @@ -0,0 +1,31 @@ +export declare enum E_FilenameError { + NONE = 0,// No error + EMPTY = 1,// Empty or whitespace-only filename + INVALID_CHAR = 2,// Contains invalid characters + RESERVED_NAME = 4,// Matches a reserved system name + LEADING_TRAILING_SPACE = 8,// Starts/ends with space + ONLY_DOTS = 16 +} +export interface I_SanitizeOptions { + lowercase?: boolean; + whitespace?: boolean; +} +export interface I_ValidationResult { + isValid: boolean; + errorFlags: number; +} +/** + * Sanitizes a filename by removing invalid characters and normalizing it. + * + * @param filename - The original filename + * @param options - Configuration options + * @returns Sanitized filename + */ +export declare function sanitizeFilename(filename?: string, options?: I_SanitizeOptions): string; +/** + * Validates a filename and returns a flag-based error representation. + * + * @param filename - The filename to validate + * @returns I_ValidationResult object with bitwise error flags + */ +export declare function validateFilename(filename: string): I_ValidationResult; diff --git a/packages/fs/dist/utils/name.js b/packages/fs/dist/utils/name.js new file mode 100644 index 00000000..8805af41 --- /dev/null +++ b/packages/fs/dist/utils/name.js @@ -0,0 +1,81 @@ +const RESERVED_NAMES = new Set([ + "con", "prn", "aux", "nul", + "com1", "com2", "com3", "com4", "com5", "com6", "com7", "com8", "com9", + "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9" +]); +// Validation error flags (bitwise) +export var E_FilenameError; +(function (E_FilenameError) { + E_FilenameError[E_FilenameError["NONE"] = 0] = "NONE"; + E_FilenameError[E_FilenameError["EMPTY"] = 1] = "EMPTY"; + E_FilenameError[E_FilenameError["INVALID_CHAR"] = 2] = "INVALID_CHAR"; + E_FilenameError[E_FilenameError["RESERVED_NAME"] = 4] = "RESERVED_NAME"; + E_FilenameError[E_FilenameError["LEADING_TRAILING_SPACE"] = 8] = "LEADING_TRAILING_SPACE"; + E_FilenameError[E_FilenameError["ONLY_DOTS"] = 16] = "ONLY_DOTS"; // Filename is only "." or ".." +})(E_FilenameError || (E_FilenameError = {})); +/** + * Sanitizes a filename by removing invalid characters and normalizing it. + * + * @param filename - The original filename + * @param options - Configuration options + * @returns Sanitized filename + */ +export function sanitizeFilename(filename = "", options = { lowercase: false, whitespace: false }) { + const { lowercase = false, whitespace = true } = options; + // Normalize Unicode (removes diacritics) + let sanitized = filename + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") // Strip accents + .replace(/[^\w.\- ]/g, "") // Keep only alphanumeric, dot, hyphen, underscore, and space + .trim(); // Remove leading/trailing spaces + // Replace spaces with underscores if enabled + if (whitespace) { + sanitized = sanitized.replace(/\s+/g, "_"); + } + // Convert to lowercase if enabled + if (lowercase) { + sanitized = sanitized.toLowerCase(); + } + // Prevent reserved names (Windows) + if (RESERVED_NAMES.has(sanitized.toLowerCase())) { + return sanitized + "_safe"; + } + // Prevent filenames that are just dots or empty + if (!sanitized || sanitized === "." || sanitized === "..") { + return "untitled"; + } + return sanitized; +} +/** + * Validates a filename and returns a flag-based error representation. + * + * @param filename - The filename to validate + * @returns I_ValidationResult object with bitwise error flags + */ +export function validateFilename(filename) { + let errorFlags = E_FilenameError.NONE; + const trimmed = filename.trim(); + if (!trimmed) { + errorFlags |= E_FilenameError.EMPTY; + } + // Detect invalid characters (only allow alphanumeric, dot, hyphen, underscore) + if (/[^a-zA-Z0-9._-]/.test(filename)) { + errorFlags |= E_FilenameError.INVALID_CHAR; + } + // Prevent reserved filenames (Windows) + if (RESERVED_NAMES.has(filename.toLowerCase())) { + errorFlags |= E_FilenameError.RESERVED_NAME; + } + // Check for leading or trailing spaces + if (/^\s|\s$/.test(filename)) { + errorFlags |= E_FilenameError.LEADING_TRAILING_SPACE; + } + // Prevent filenames that are only "." or ".." + if (filename === "." || filename === "..") { + errorFlags |= E_FilenameError.ONLY_DOTS; + } + return { + isValid: errorFlags === E_FilenameError.NONE, + errorFlags + }; +} diff --git a/packages/fs/package.json b/packages/fs/package.json index 6d2b0433..40378d12 100644 --- a/packages/fs/package.json +++ b/packages/fs/package.json @@ -7,6 +7,14 @@ "access": "public" }, "exports": { + "./utils": { + "import": "./dist/utils/index.js", + "require": "./dist/utils/index.cjs" + }, + "./append": { + "import": "./dist/append.js", + "require": "./dist/append.cjs" + }, "./write": { "import": "./dist/write.js", "require": "./dist/write.cjs" diff --git a/packages/fs/src/file.ts b/packages/fs/src/file.ts index 1a311ab2..6d63c9f8 100644 --- a/packages/fs/src/file.ts +++ b/packages/fs/src/file.ts @@ -136,7 +136,6 @@ const checkContentAsync = (path: string, mode: string, options: IOptions):Promis const writeAsync = async (path: string, stat: Stats, options: IOptions): Promise => { const mode = normalizeFileMode(stat.mode); return checkContentAsync(path, mode, options) - //.then() } const touchAsync = (path: string, options: IOptions) => { diff --git a/packages/fs/src/index.ts b/packages/fs/src/index.ts index 67e59fb0..5c52521c 100644 --- a/packages/fs/src/index.ts +++ b/packages/fs/src/index.ts @@ -29,111 +29,3 @@ import * as stats from './stats.js' import { ICopyOptions, INode, IInspectOptions } from './interfaces.js' import { ReadWriteDataType, TCopyResult, ENodeType, TDeleteResult } from './interfaces.js' - -export interface IJetpack { - cwd(w?: any): IJetpack | string; - path(): string; - append(path: string, data: string | Buffer | object, options?: AppendOptions): void; - appendAsync(path: string, data: string | Buffer | object, options?: AppendOptions): Promise; - copy(from: string, to: string, options?: ICopyOptions): void; - copyAsync(from: string, to: string, options?: ICopyOptions): Promise; - createWriteStream(path: string, options?: { - flags?: string; - encoding?: string; - fd?: number; - mode?: number; - autoClose?: boolean; - start?: number; - }): any; - createReadStream(path: string, options?: { - flags?: string; - encoding?: string; - fd?: number; - mode?: number; - autoClose?: boolean; - start?: number; - end?: number; - }): any; - dir(path: string, criteria?: DirOptions): IJetpack; - dirAsync(path: string, criteria?: DirOptions): Promise; - exists(path: string): boolean | string; - existsAsync(path: string): Promise; - file(path: string, criteria?: FileOptions): void; - fileAsync(path: string, criteria?: FileOptions): Promise; - find(startPath: string, options: FindOptions): string[]; - findAsync(startPath: string, options: FindOptions): Promise; - inspect(path: string, fieldsToInclude: IInspectOptions): INode; - inspectAsync(path: string, fieldsToInclude: IInspectOptions): Promise; - inspectTree(path: string, options?: InspectTreeOptions): INode; - inspectTreeAsync(path: string, options?: InspectTreeOptions): Promise; - list(path: string): string[]; - listAsync(path: string): Promise; - move(from: string, to: string): void; - moveAsync(from: string, to: string): Promise; - read(path: string, returnAs?: string): ReadWriteDataType; - readAsync(path: string, returnAs?: string): Promise; - remove(path: string): void; - removeAsync(path: string): Promise; - rename(path: string, newName: string): void; - renameAsync(path: string, newName: string): Promise; - symlink(symlinkValue: string, path: string): void; - symlinkAsync(symlinkValue: string, path: string): Promise; - write(path: string, data: string | Buffer | object, options?: IWriteOptions): void; - writeAsync(path: string, data: string | Buffer | object, options?: IWriteOptions): Promise; -} -// The Jetpack Context object. -// It provides the public API, and resolves all paths regarding to -// passed cwdPath, or default process.cwd() if cwdPath was not specified. -export const fs = (cwdPath?: string): IJetpack => { - const getCwdPath = function () { - return cwdPath || process.cwd(); - }; - - const cwd = function (w?: any): IJetpack | string { - let args; - let pathParts; - - // return current CWD if no arguments specified... - if (arguments.length === 0) { - return getCwdPath(); - } - - // ...create new CWD context otherwise - args = Array.prototype.slice.call(arguments); - pathParts = [getCwdPath()].concat(args); - const res = fs(pathUtil.resolve.apply(null, pathParts)); - return res; - }; - - // resolves path to inner CWD path of this jetpack instance - const resolvePath = function (path: string): string { - return pathUtil.resolve(getCwdPath(), path); - }; - - const getPath = function (): string { - // add CWD base path as first element of arguments array - Array.prototype.unshift.call(arguments, getCwdPath()); - return pathUtil.resolve.apply(null, arguments); - }; - - const normalizeOptions = function (options: { cwd?: string }): any { - return options || { cwd: getCwdPath() }; - }; - - // API - const api: any = { - - }; - if ((util.inspect as any)['custom'] !== undefined) { - // Without this console.log(jetpack) throws obscure error. Details: - // https://github.com/szwacz/fs-jetpack/issues/29 - // https://nodejs.org/api/util.html#util_custom_inspection_functions_on_objects - (api as any)[(util.inspect as any)['custom']] = function () { - return getCwdPath(); - }; - } - - return api; -}; - -export default fs; diff --git a/packages/fs/src/main.ts b/packages/fs/src/main.ts deleted file mode 100644 index 9c1d75d4..00000000 --- a/packages/fs/src/main.ts +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env node -import * as cli from 'yargs' - -const argv: any = cli.argv - -if (argv.h || argv.help) { - cli.showHelp(); - process.exit(); -} else if (argv.v || argv.version) { - process.exit(); -} diff --git a/packages/fs/src/utils/dot.ts b/packages/fs/src/utils/dot.ts index cf6ac8d5..c3973147 100644 --- a/packages/fs/src/utils/dot.ts +++ b/packages/fs/src/utils/dot.ts @@ -8,3 +8,4 @@ export function isDotFile(str: string) { const last = str.lastIndexOf('/'); return last !== -1 ? str.charCodeAt(last + 1) === 46 /* . */ : false; } + \ No newline at end of file diff --git a/packages/fs/src/utils/index.ts b/packages/fs/src/utils/index.ts new file mode 100644 index 00000000..d26a621c --- /dev/null +++ b/packages/fs/src/utils/index.ts @@ -0,0 +1,6 @@ +export * from "./wildcard.js" +export * from "./paths.js" +export * from "./name.js" +export * from "./matcher.js" +export * from "./mime_match.js" +export * from "./validate.js" diff --git a/packages/fs/src/utils/name.ts b/packages/fs/src/utils/name.ts new file mode 100644 index 00000000..a12d4be7 --- /dev/null +++ b/packages/fs/src/utils/name.ts @@ -0,0 +1,106 @@ +const RESERVED_NAMES = new Set([ + "con", "prn", "aux", "nul", + "com1", "com2", "com3", "com4", "com5", "com6", "com7", "com8", "com9", + "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9" +]); + +// Validation error flags (bitwise) +export enum E_FilenameError { + NONE = 0, // No error + EMPTY = 1 << 0, // Empty or whitespace-only filename + INVALID_CHAR = 1 << 1, // Contains invalid characters + RESERVED_NAME = 1 << 2, // Matches a reserved system name + LEADING_TRAILING_SPACE = 1 << 3, // Starts/ends with space + ONLY_DOTS = 1 << 4 // Filename is only "." or ".." +} + +export interface I_SanitizeOptions { + lowercase?: boolean; // Convert to lowercase (default: false) + whitespace?: boolean; // Replace spaces with underscores (default: true) +} + +export interface I_ValidationResult { + isValid: boolean; // Overall validation status + errorFlags: number; // Bitwise error representation +} + +/** + * Sanitizes a filename by removing invalid characters and normalizing it. + * + * @param filename - The original filename + * @param options - Configuration options + * @returns Sanitized filename + */ +export function sanitizeFilename(filename: string = "", options: I_SanitizeOptions = { lowercase: false, whitespace: false }): string { + + const { lowercase = false, whitespace = true } = options; + + // Normalize Unicode (removes diacritics) + let sanitized = filename + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") // Strip accents + .replace(/[^\w.\- ]/g, "") // Keep only alphanumeric, dot, hyphen, underscore, and space + .trim(); // Remove leading/trailing spaces + + // Replace spaces with underscores if enabled + if (whitespace) { + sanitized = sanitized.replace(/\s+/g, "_"); + } + + // Convert to lowercase if enabled + if (lowercase) { + sanitized = sanitized.toLowerCase(); + } + + // Prevent reserved names (Windows) + if (RESERVED_NAMES.has(sanitized.toLowerCase())) { + return sanitized + "_safe"; + } + + // Prevent filenames that are just dots or empty + if (!sanitized || sanitized === "." || sanitized === "..") { + return "untitled"; + } + + return sanitized; +} + +/** + * Validates a filename and returns a flag-based error representation. + * + * @param filename - The filename to validate + * @returns I_ValidationResult object with bitwise error flags + */ +export function validateFilename(filename: string): I_ValidationResult { + let errorFlags = E_FilenameError.NONE; + const trimmed = filename.trim(); + + if (!trimmed) { + errorFlags |= E_FilenameError.EMPTY; + } + + // Detect invalid characters (only allow alphanumeric, dot, hyphen, underscore) + if (/[^a-zA-Z0-9._-]/.test(filename)) { + errorFlags |= E_FilenameError.INVALID_CHAR; + } + + // Prevent reserved filenames (Windows) + if (RESERVED_NAMES.has(filename.toLowerCase())) { + errorFlags |= E_FilenameError.RESERVED_NAME; + } + + // Check for leading or trailing spaces + if (/^\s|\s$/.test(filename)) { + errorFlags |= E_FilenameError.LEADING_TRAILING_SPACE; + } + + // Prevent filenames that are only "." or ".." + if (filename === "." || filename === "..") { + errorFlags |= E_FilenameError.ONLY_DOTS; + } + + return { + isValid: errorFlags === E_FilenameError.NONE, + errorFlags + }; +} \ No newline at end of file