/** * Helper utilities for SMB2 Server * Common utility functions used throughout the server */ import { randomBytes, createHash } from 'crypto'; import { resolve, normalize, join } from 'path'; import type { SMB2Header, SMB2FileId, SMB2Guid, } from '../protocol/types.js'; import { SMB2_PROTOCOL_ID, SMB2_HEADER_SIZE, SMB2_COMMANDS, SMB2_STATUS, } from '../protocol/constants.js'; /** * Generate a random SMB2 GUID */ export function generateGuid(): SMB2Guid { const bytes = randomBytes(16); const hex = bytes.toString('hex').toUpperCase(); // Format as standard GUID: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX return [ hex.substring(0, 8), hex.substring(8, 12), hex.substring(12, 16), hex.substring(16, 20), hex.substring(20, 32), ].join('-'); } /** * Convert GUID string to bytes */ export function guidToBytes(guid: SMB2Guid): Buffer { const hex = guid.replace(/-/g, ''); if (hex.length !== 32) { throw new Error('Invalid GUID format'); } return Buffer.from(hex, 'hex'); } /** * Convert bytes to GUID string */ export function bytesToGuid(bytes: Buffer): SMB2Guid { if (bytes.length !== 16) { throw new Error('GUID must be exactly 16 bytes'); } const hex = bytes.toString('hex').toUpperCase(); return [ hex.substring(0, 8), hex.substring(8, 12), hex.substring(12, 16), hex.substring(16, 20), hex.substring(20, 32), ].join('-'); } /** * Generate a random SMB2 File ID */ export function generateFileId(): SMB2FileId { return { persistent: BigInt(`0x${randomBytes(8).toString('hex')}`), volatile: BigInt(`0x${randomBytes(8).toString('hex')}`), }; } /** * Convert File ID to string for use as map key */ export function fileIdToString(fileId: SMB2FileId): string { return `${fileId.persistent.toString(16)}-${fileId.volatile.toString(16)}`; } /** * Parse File ID from string */ export function stringToFileId(str: string): SMB2FileId { const parts = str.split('-'); if (parts.length !== 2) { throw new Error('Invalid File ID string format'); } return { persistent: BigInt(`0x${parts[0]}`), volatile: BigInt(`0x${parts[1]}`), }; } /** * Validate SMB2 header structure */ export function validateSMB2Header(header: SMB2Header): boolean { // Check protocol ID if (header.protocolId !== SMB2_PROTOCOL_ID) { return false; } // Check structure size if (header.structureSize !== SMB2_HEADER_SIZE) { return false; } // Check if command is valid if (!Object.values(SMB2_COMMANDS).includes(header.command)) { return false; } // Check signature length if (header.signature.length !== 16) { return false; } return true; } /** * Convert Windows FILETIME to JavaScript Date */ export function fileTimeToDate(filetime: bigint): Date { // FILETIME is 100-nanosecond intervals since January 1, 1601 const FILETIME_EPOCH_DIFF = 11644473600000n; // milliseconds between 1601 and 1970 const timestamp = Number(filetime / 10000n) - Number(FILETIME_EPOCH_DIFF); return new Date(timestamp); } /** * Convert JavaScript Date to Windows FILETIME */ export function dateToFileTime(date: Date): bigint { // FILETIME is 100-nanosecond intervals since January 1, 1601 const FILETIME_EPOCH_DIFF = 11644473600000n; // milliseconds between 1601 and 1970 const timestamp = BigInt(date.getTime()) + FILETIME_EPOCH_DIFF; return timestamp * 10000n; // Convert to 100-nanosecond intervals } /** * Get current time as FILETIME */ export function getCurrentFileTime(): bigint { return dateToFileTime(new Date()); } /** * Pad buffer to specified alignment */ export function padBuffer(buffer: Buffer, alignment: number): Buffer { const padding = (alignment - (buffer.length % alignment)) % alignment; if (padding === 0) { return buffer; } const paddingBuffer = Buffer.alloc(padding); return Buffer.concat([buffer, paddingBuffer]); } /** * Calculate padding needed for alignment */ export function calculatePadding(length: number, alignment: number): number { return (alignment - (length % alignment)) % alignment; } /** * Convert UTF-16LE string to UTF-8 */ export function utf16leToUtf8(buffer: Buffer): string { return buffer.toString('utf16le'); } /** * Convert UTF-8 string to UTF-16LE buffer */ export function utf8ToUtf16le(str: string): Buffer { return Buffer.from(str, 'utf16le'); } /** * Normalize SMB path (convert backslashes to forward slashes) */ export function normalizeSmbPath(path: string): string { return path.replace(/\\/g, '/').replace(/\/+/g, '/'); } /** * Convert SMB path to local file system path */ export function smbPathToLocal(smbPath: string, shareRoot: string): string { const normalized = normalizeSmbPath(smbPath); const relativePath = normalized.startsWith('/') ? normalized.substring(1) : normalized; return resolve(shareRoot, relativePath); } /** * Convert local file system path to SMB path */ export function localPathToSmb(localPath: string, shareRoot: string): string { const relativePath = localPath.replace(shareRoot, '').replace(/\\/g, '/'); return relativePath.startsWith('/') ? relativePath : `/${relativePath}`; } /** * Validate SMB path for security (prevent directory traversal) */ export function validateSmbPath(path: string): boolean { const normalized = normalize(path); // Check for directory traversal attempts if (normalized.includes('..') || normalized.includes('//')) { return false; } // Check for invalid characters (basic validation) const invalidChars = /[<>:"|?*\x00-\x1f]/; if (invalidChars.test(normalized)) { return false; } return true; } /** * Extract filename from SMB path */ export function extractFilename(path: string): string { const normalized = normalizeSmbPath(path); const parts = normalized.split('/'); return parts[parts.length - 1] || ''; } /** * Extract directory from SMB path */ export function extractDirectory(path: string): string { const normalized = normalizeSmbPath(path); const parts = normalized.split('/'); parts.pop(); // Remove filename return parts.join('/') || '/'; } /** * Join SMB paths */ export function joinSmbPaths(...paths: string[]): string { return normalizeSmbPath(paths.join('/')); } /** * Check if buffer contains only zeros */ export function isZeroBuffer(buffer: Buffer): boolean { return buffer.every(byte => byte === 0); } /** * Generate random bytes of specified length */ export function generateRandomBytes(length: number): Buffer { return randomBytes(length); } /** * Calculate MD5 hash of data */ export function calculateMd5(data: Buffer | string): string { return createHash('md5').update(data).digest('hex'); } /** * Calculate SHA256 hash of data */ export function calculateSha256(data: Buffer | string): string { return createHash('sha256').update(data).digest('hex'); } /** * Compare two buffers in constant time (to prevent timing attacks) */ export function constantTimeCompare(a: Buffer, b: Buffer): boolean { if (!a || !b || a.length !== b.length || a.length === 0 || b.length === 0) { return false; } let result = 0; for (let i = 0; i < a.length; i++) { result |= a[i] ^ b[i]; } return result === 0; } /** * Format bytes as human readable string */ export function formatBytes(bytes: number): string { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; } /** * Format duration in milliseconds as human readable string */ export function formatDuration(ms: number): string { if (ms < 1000) return `${ms}ms`; if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`; if (ms < 3600000) return `${(ms / 60000).toFixed(1)}m`; return `${(ms / 3600000).toFixed(1)}h`; } /** * Sleep for specified milliseconds */ export function sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Create a timeout promise that rejects after specified time */ export function timeout(promise: Promise, ms: number): Promise { const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error('Timeout')), ms); }); return Promise.race([promise, timeoutPromise]); } /** * Retry a function with exponential backoff */ export async function retry( fn: () => Promise, maxAttempts: number = 3, baseDelay: number = 1000 ): Promise { let lastError: Error; for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { return await fn(); } catch (error) { lastError = error as Error; if (attempt === maxAttempts) { throw lastError; } const delay = baseDelay * Math.pow(2, attempt - 1); await sleep(delay); } } throw lastError!; } /** * Clamp a number between min and max values */ export function clamp(value: number, min: number, max: number): number { return Math.min(Math.max(value, min), max); } /** * Check if a value is null or undefined */ export function isNullOrUndefined(value: any): value is null | undefined { return value === null || value === undefined; } /** * Deep clone an object (simple implementation) */ export function deepClone(obj: T): T { if (obj === null || typeof obj !== 'object') { return obj; } if (obj instanceof Date) { return new Date(obj.getTime()) as any; } if (obj instanceof Buffer) { return Buffer.from(obj) as any; } if (Array.isArray(obj)) { return obj.map(item => deepClone(item)) as any; } const cloned = {} as T; for (const key in obj) { if (obj.hasOwnProperty(key)) { cloned[key] = deepClone(obj[key]); } } return cloned; } /** * Safe JSON parse that returns null on error */ export function safeJsonParse(str: string): T | null { try { return JSON.parse(str); } catch { return null; } } /** * Safe JSON stringify that returns empty string on error */ export function safeJsonStringify(obj: any): string { try { return JSON.stringify(obj); } catch { return ''; } } /** * Get environment variable with default value */ export function getEnvVar(name: string, defaultValue: string = ''): string { return process.env[name] ?? defaultValue; } /** * Get environment variable as number with default value */ export function getEnvNumber(name: string, defaultValue: number = 0): number { const value = process.env[name]; if (!value) return defaultValue; const parsed = parseInt(value, 10); return isNaN(parsed) ? defaultValue : parsed; } /** * Get environment variable as boolean with default value */ export function getEnvBoolean(name: string, defaultValue: boolean = false): boolean { const value = process.env[name]; if (!value) return defaultValue; return value.toLowerCase() === 'true' || value === '1'; } /** * Debounce function calls */ export function debounce any>( func: T, delay: number ): (...args: Parameters) => void { let timeoutId: NodeJS.Timeout; return (...args: Parameters) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => func.apply(null, args), delay); }; } /** * Throttle function calls */ export function throttle any>( func: T, limit: number ): (...args: Parameters) => void { let inThrottle: boolean; return (...args: Parameters) => { if (!inThrottle) { func.apply(null, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; } /** * Create a simple event emitter */ export class SimpleEventEmitter> { private listeners: Map void>> = new Map(); on(event: K, listener: (...args: T[K]) => void): void { if (!this.listeners.has(event)) { this.listeners.set(event, []); } this.listeners.get(event)!.push(listener); } off(event: K, listener: (...args: T[K]) => void): void { const eventListeners = this.listeners.get(event); if (eventListeners) { const index = eventListeners.indexOf(listener); if (index > -1) { eventListeners.splice(index, 1); } } } emit(event: K, ...args: T[K]): void { const eventListeners = this.listeners.get(event); if (eventListeners) { eventListeners.forEach(listener => listener(...args)); } } removeAllListeners(event?: K): void { if (event) { this.listeners.delete(event); } else { this.listeners.clear(); } } }