import chardet from "chardet"; import dayjs from "dayjs"; import iconvLite from "iconv-lite"; import {Base64} from "js-base64"; import * as crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; import Showdown from "showdown"; // import {Iconv} from "iconv" import {isMac, isWin} from "./env"; import FileIndex from "../mapi/file"; export const EncodeUtil = { base32Alphabet: "abcdefghijklmnopqrstuvwxyz234567", base32Encode(str: string) { const buffer = Buffer.from(str, "utf8"); let bits = ""; let output = ""; // 将每个字节转为8位二进制 for (let i = 0; i < buffer.length; i++) { const byte = buffer[i]; bits += byte.toString(2).padStart(8, "0"); } // 每5位一组,转为 Base32 字符 for (let i = 0; i < bits.length; i += 5) { const chunk = bits.slice(i, i + 5); const paddedChunk = chunk.padEnd(5, "0"); // 不足5位补0 const index = parseInt(paddedChunk, 2); output += EncodeUtil.base32Alphabet[index]; } return output; }, base32Decode(str: string) { const base32Alphabet = "abcdefghijklmnopqrstuvwxyz234567"; let bits = ""; for (let i = 0; i < str.length; i++) { const char = str[i]; const index = base32Alphabet.indexOf(char); if (index === -1) { throw new Error("Invalid Base32 character: " + char); } bits += index.toString(2).padStart(5, "0"); } const bytes: number[] = []; for (let i = 0; i + 8 <= bits.length; i += 8) { const byte = bits.slice(i, i + 8); bytes.push(parseInt(byte, 2)); } return Buffer.from(bytes).toString("utf8"); }, base64Encode(str: string) { return Base64.encode(str); }, base64Decode(str: string) { return Base64.decode(str); }, md5(str: string) { return crypto.createHash("md5").update(str).digest("hex"); }, aesEncode(str: string, key: string) { const cipher = crypto.createCipheriv("aes-128-ecb", key, ""); let crypted = cipher.update(str, "utf8", "base64"); crypted += cipher.final("base64"); return crypted; }, aesDecode(str: string, key: string) { const decipher = crypto.createDecipheriv("aes-128-ecb", key, ""); let dec = decipher.update(str, "base64", "utf8"); dec += decipher.final("utf8"); return dec; }, async fileXzipEncode(pathname: string): Promise { if (!fs.existsSync(pathname)) { throw new Error(`Input file not found: ${pathname}`); } // Generate new filepath with .xzip extension const basePath = pathname.substring(0, pathname.lastIndexOf(".")); const outputPath = basePath + ".xzip"; // Get file info const fileStats = fs.statSync(pathname); const fileSize = fileStats.size; const fileExt = pathname.split(".").pop() || ""; // Generate random 16-character key const encryptionKey = StrUtil.randomString(16); // Create metadata const filemeta = { version: 1, format: fileExt, size: fileSize, key: encryptionKey, }; // Convert metadata to JSON and then base64 encode const metaJson = JSON.stringify(filemeta); const metaB64 = Buffer.from(metaJson, "utf-8").toString("base64"); const metaLength = metaB64.length; // Prepare encryption key const keyBytes = Buffer.from(encryptionKey, "utf-8"); const keyLength = keyBytes.length; // Stream processing: read, encrypt and write in chunks const inputStream = fs.createReadStream(pathname); const outputStream = fs.createWriteStream(outputPath); // Write metadata length (4 bytes, little-endian) const metaLengthBuffer = Buffer.allocUnsafe(4); metaLengthBuffer.writeUInt32LE(metaLength, 0); outputStream.write(metaLengthBuffer); // Write base64 encoded metadata outputStream.write(Buffer.from(metaB64, "utf-8")); // Stream encrypt the file content let bytesProcessed = 0; return new Promise((resolve, reject) => { inputStream.on("data", (chunk: Buffer) => { // XOR encrypt the chunk const encryptedChunk = Buffer.alloc(chunk.length); for (let i = 0; i < chunk.length; i++) { encryptedChunk[i] = chunk[i] ^ keyBytes[bytesProcessed % keyLength]; bytesProcessed++; } // Write encrypted chunk outputStream.write(encryptedChunk); }); inputStream.on("end", () => { outputStream.end(); resolve(outputPath); }); inputStream.on("error", error => { outputStream.destroy(); reject(error); }); outputStream.on("error", error => { inputStream.destroy(); reject(error); }); }); }, async fileXzipDecode(pathname: string): Promise { if (!fs.existsSync(pathname)) { throw new Error(`Input file not found: ${pathname}`); } if (!pathname.endsWith(".xzip")) { return pathname; // Not an xzip file, return as is } let outputPath = pathname.replace(/\.xzip$/, ""); return new Promise((resolve, reject) => { const inputStream = fs.createReadStream(pathname); let metadataRead = false; let filemeta: any = null; let keyBytes: Buffer; let bytesProcessed = 0; let outputStream: fs.WriteStream; let remainingMetaBytes = 0; let metaBuffer = Buffer.alloc(0); inputStream.on("data", (chunk: Buffer) => { let chunkOffset = 0; if (!metadataRead) { if (remainingMetaBytes === 0) { // Read metadata length (first 4 bytes) if (chunk.length < 4) { reject(new Error("Invalid xzip file: insufficient data for metadata length")); return; } const metaLength = chunk.readUInt32LE(0); remainingMetaBytes = metaLength; chunkOffset = 4; } // Read metadata const availableMetaBytes = Math.min(remainingMetaBytes, chunk.length - chunkOffset); const metaChunk = chunk.subarray(chunkOffset, chunkOffset + availableMetaBytes); metaBuffer = Buffer.concat([metaBuffer, metaChunk] as readonly Uint8Array[]); remainingMetaBytes -= availableMetaBytes; chunkOffset += availableMetaBytes; if (remainingMetaBytes === 0) { // Parse metadata try { const metaB64 = metaBuffer.toString("utf-8"); const metaJson = Buffer.from(metaB64, "base64").toString("utf-8"); filemeta = JSON.parse(metaJson); keyBytes = Buffer.from(filemeta.key, "utf-8"); // Create output file with correct extension const finalOutputPath = outputPath + (filemeta.format ? "." + filemeta.format : ""); outputStream = fs.createWriteStream(finalOutputPath); metadataRead = true; // Set the final output path for resolution outputPath = finalOutputPath; } catch (error) { reject(new Error("Invalid xzip file: corrupted metadata")); return; } } } if (metadataRead && chunkOffset < chunk.length) { // Decrypt remaining chunk data const encryptedChunk = chunk.subarray(chunkOffset); const decryptedChunk = Buffer.alloc(encryptedChunk.length); const keyLength = keyBytes.length; for (let i = 0; i < encryptedChunk.length; i++) { decryptedChunk[i] = encryptedChunk[i] ^ keyBytes[bytesProcessed % keyLength]; bytesProcessed++; } outputStream.write(decryptedChunk); } }); inputStream.on("end", () => { if (outputStream) { outputStream.end(); resolve(outputPath); } else { reject(new Error("Invalid xzip file: incomplete metadata")); } }); inputStream.on("error", error => { if (outputStream) { outputStream.destroy(); } reject(error); }); if (outputStream) { outputStream.on("error", error => { inputStream.destroy(); reject(error); }); } }); }, }; export const IconvUtil = { convert(str: string, to?: string, from?: string) { if (!from) { from = chardet.detect(Buffer.from(str)); } to = to || "utf8"; const buffer = iconvLite.encode(str, from); return iconvLite.decode(buffer, to); }, bufferToUtf8(buffer: Buffer) { const encoding = chardet.detect(buffer); // if ('ISO-2022-CN' === encoding) { // const iconvInstance = new Iconv('ISO-2022-CN', 'UTF-8//TRANSLIT//IGNORE'); // return iconvInstance.convert(buffer).toString() // } return iconvLite.decode(buffer, encoding).toString(); }, detect(buffer: Uint8Array) { // detect str encoding return chardet.detect(buffer); }, }; export const StrUtil = { randomString(len: number = 32) { const chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; let result = ""; for (let i = len; i > 0; --i) { result += chars[Math.floor(Math.random() * chars.length)]; } return result; }, uuid() { return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { const r = (Math.random() * 16) | 0; const v = c === "x" ? r : (r & 0x3) | 0x8; return v.toString(16); }); }, hashCode(str: string, length: number = 8) { let hash = 0; if (str.length === 0) return hash + ""; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = (hash << 5) - hash + char; hash = hash & hash; } let result = Math.abs(hash).toString(16); if (result.length < length) { result = "0".repeat(length - result.length) + result; } return result; }, hashCodeWithDuplicateCheck(str: string, check: string[], length: number = 8) { let code = this.hashCode(str, length); while (check.includes(code)) { code = this.uuid().substring(0, length); } return code; }, bigIntegerId() { return [Date.now(), (Math.floor(Math.random() * 1000000) + "").padStart(6, "0")].join(""); }, ucFirst(str: string) { if (!str) return ""; return str.charAt(0).toUpperCase() + str.slice(1); }, }; export const TimeUtil = { timestampInMs() { return Date.now(); }, timestamp() { return Math.floor(Date.now() / 1000); }, format(time: number, format: string = "YYYY-MM-DD HH:mm:ss") { return dayjs(time).format(format); }, formatDate(time: number) { return dayjs(time).format("YYYY-MM-DD"); }, dateString() { return dayjs().format("YYYYMMDD"); }, datetimeString() { return dayjs().format("YYYYMMDD_HHmmss_SSS"); }, timestampDayStart(msTimestamp?: number) { let date = msTimestamp ? new Date(msTimestamp) : new Date(); date.setHours(0, 0, 0, 0); return Math.floor(date.getTime() / 1000); }, replacePattern(text: string) { // @ts-ignore return text.replaceAll("{year}", dayjs().format("YYYY")) .replaceAll("{month}", dayjs().format("MM")) .replaceAll("{day}", dayjs().format("DD")) .replaceAll("{hour}", dayjs().format("HH")) .replaceAll("{minute}", dayjs().format("mm")) .replaceAll("{second}", dayjs().format("ss")); }, }; export const FileUtil = { MIME_TYPES: { html: "text/html", htm: "text/html", js: "application/javascript", css: "text/css", json: "application/json", png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", svg: "image/svg+xml", webp: "image/webp", woff: "font/woff", woff2: "font/woff2", ttf: "font/ttf", otf: "font/otf", mp3: "audio/mpeg", mp4: "video/mp4", wav: "audio/wav", wasm: "application/wasm", eot: "application/vnd.ms-fontobject", }, getMimeByExt(ext: string, defaultMime: string = ""): string { ext = ext.toLowerCase(); if (ext.startsWith(".")) { ext = ext.substring(1); } return FileUtil.MIME_TYPES[ext] || defaultMime; }, getMimeByPath(p: string, defaultMime: string = ""): string { const extension = p.split(".").pop().toLowerCase(); return FileUtil.getMimeByExt(extension, defaultMime); }, streamToBase64(stream: NodeJS.ReadableStream): Promise { return new Promise((resolve, reject) => { const chunks = []; stream.on("data", chunk => { chunks.push(chunk); }); stream.on("end", () => { const buffer = Buffer.concat(chunks); resolve(buffer.toString("base64")); }); stream.on("error", error => { reject(error); }); }); }, bufferToBase64(buffer: Buffer) { let binary = ""; let bytes = new Uint8Array(buffer); let len = bytes.byteLength; for (let i = 0; i < len; i++) { binary += String.fromCharCode(bytes[i]); } return EncodeUtil.base64Encode(binary); }, base64ToBuffer(base64: string): Buffer { if (base64.startsWith("data:")) { base64 = base64.split("base64,")[1]; } return Buffer.from(base64, "base64"); }, formatSize(size: number) { if (size < 1024) { return size + "B"; } else if (size < 1024 * 1024) { return (size / 1024).toFixed(2) + "KB"; } else if (size < 1024 * 1024 * 1024) { return (size / 1024 / 1024).toFixed(2) + "MB"; } else { return (size / 1024 / 1024 / 1024).toFixed(2) + "GB"; } }, async md5(filePath: string) { return new Promise((resolve, reject) => { const hash = crypto.createHash("md5"); const stream = fs.createReadStream(filePath); stream.on("data", data => { hash.update(data); }); stream.on("end", () => { resolve(hash.digest("hex")); }); stream.on("error", error => { reject(error); }); }); }, }; export const JsonUtil = { stringifyOrdered(obj: any) { return JSON.stringify(obj, Object.keys(obj).sort(), 4); }, stringifyValueOrdered(obj: any) { const sortedData = Object.fromEntries( Object.entries(obj).sort(([, a], [, b]) => { // @ts-ignore return ((a as any) - b) as any; }) ); return JSON.stringify(sortedData, null, 4); }, }; export const ImportUtil = { async loadCommonJs(cjsPath: string, forceReload: boolean = true) { let tempPath = cjsPath; if (forceReload) { const md5 = await FileUtil.md5(cjsPath); tempPath = path.join( await FileIndex.tempDir('commonJs'), `${md5}.cjs`, ) if (!fs.existsSync(tempPath)) { fs.copyFileSync(cjsPath, tempPath); } } const backend = await import(/* @vite-ignore */ `file://${tempPath}`); // console.log('loadCommonJs', `${cjsPath}?t=${md5}`) return backend.default; }, }; export const MemoryCacheUtil = { pool: {} as { [key: string]: { value: any; expire: number; }; }, _gc() { const now = TimeUtil.timestamp(); for (const key in this.pool) { if (this.pool[key].expire < now) { delete this.pool[key]; } } }, async remember(key: string, callback: () => Promise, ttl: number = 60) { if (this.pool[key] && this.pool[key].expire > TimeUtil.timestamp()) { return this.pool[key].value; } const value = await callback(); this.pool[key] = { value, expire: TimeUtil.timestamp() + ttl, }; this._gc(); return value; }, get(key: string) { if (this.pool[key] && this.pool[key].expire > TimeUtil.timestamp()) { return this.pool[key].value; } this._gc(); return null; }, set(key: string, value: any, ttl: number = 86400) { this.pool[key] = { value, expire: TimeUtil.timestamp() + ttl, }; this._gc(); }, forget(key: string) { delete this.pool[key]; }, }; export const MemoryMapCacheUtil = { pool: {} as { [group: string]: { [key: string]: { value: any; expire: number; }; }; }, _gc() { const now = TimeUtil.timestamp(); for (const group in this.pool) { for (const key in this.pool[group]) { if (this.pool[group][key].expire < now) { delete this.pool[group][key]; } } } }, get(group: string, key: string) { if (this.pool[group] && this.pool[group][key] && this.pool[group][key].expire > TimeUtil.timestamp()) { return this.pool[group][key].value; } this._gc(); return null; }, set(group: string, key: string, value: any, ttl: number = 86400) { if (!this.pool[group]) { this.pool[group] = {}; } this.pool[group][key] = { value, expire: TimeUtil.timestamp() + ttl, }; this._gc(); }, forget(group: string, key: string) { if (this.pool[group]) { delete this.pool[group][key]; } }, }; export const ShellUtil = { quotaPath(p: string) { return `"${p}"`; }, parseCommandArgs(command: string) { let args = []; let arg = ""; let quote = ""; let escape = false; for (let i = 0; i < command.length; i++) { const c = command[i]; if (escape) { arg += c; escape = false; continue; } if ("\\" === c) { escape = true; arg += c; continue; } if ("" === quote && (" " === c || "\t" === c)) { if (arg) { args.push(arg); arg = ""; } continue; } if ("" === quote && ('"' === c || "'" === c)) { quote = c; arg += c; continue; } if ('"' === quote && '"' === c) { quote = ""; arg += c; continue; } if ("'" === quote && "'" === c) { quote = ""; arg += c; continue; } arg += c; } if (arg) { args.push(arg); } return args; }, }; export const VersionUtil = { /** * 检测版本是否匹配 * @param v string * @param match string 如 * 或 >=1.0.0 或 >1.0.0 或 <1.0.0 或 <=1.0.0 或 1.0.0 */ match(v: string, match: string) { if (match === "*") { return true; } if (match.startsWith(">=")) { if (this.ge(v, match.substring(2))) { return true; } } else if (match.startsWith("<=")) { if (this.le(v, match.substring(2))) { return true; } } else if (match.startsWith(">")) { if (this.gt(v, match.substring(1))) { return true; } } else if (match.startsWith("<")) { if (this.lt(v, match.substring(1))) { return true; } } else { return this.eq(v, match); } return false; }, compare(v1: string, v2: string) { const v1Arr = v1.split("."); const v2Arr = v2.split("."); for (let i = 0; i < v1Arr.length; i++) { const v1Num = parseInt(v1Arr[i]); const v2Num = parseInt(v2Arr[i]); if (v1Num > v2Num) { return 1; } else if (v1Num < v2Num) { return -1; } } return 0; }, gt(v1: string, v2: string) { return VersionUtil.compare(v1, v2) > 0; }, ge(v1: string, v2: string) { return VersionUtil.compare(v1, v2) >= 0; }, lt(v1: string, v2: string) { return VersionUtil.compare(v1, v2) < 0; }, le: (v1: string, v2: string) => { return VersionUtil.compare(v1, v2) <= 0; }, eq: (v1: string, v2: string) => { return VersionUtil.compare(v1, v2) === 0; }, }; export const UIUtil = { sizeToPx(size: string, sizeFull: number) { if (/^\d+$/.test(size)) { // 纯数字 return parseInt(size); } else if (size.endsWith("%")) { // 百分比 let result = Math.floor((sizeFull * parseInt(size)) / 100); result = Math.min(result, sizeFull); return result; } else { throw "UnsupportSizeString"; } }, }; export const ReUtil = { match(regex: string, text: string) { if ("" === regex || null === regex) { return false; } if (regex.startsWith("/")) { const index = regex.lastIndexOf("/"); const source = regex.slice(1, index); const flags = regex.slice(index + 1); return new RegExp(source, flags).test(text); } return new RegExp(regex).test(text); }, }; const converter = new Showdown.Converter({ tables: true, }); export const MarkdownUtil = { toHtml(markdown: string): string { return converter.makeHtml(markdown); }, }; type HotkeyModifierType = "Control" | "Option" | "Command" | "Ctrl" | "Alt" | "Win" | "Meta" | "Shift"; type HotkeyType = { key: string; modifiers: HotkeyModifierType[] }; export const HotKeyUtil = { orderModifiers(modifiers: HotkeyModifierType[]) { const order = ["Control", "Ctrl", "Command", "Meta", "Win", "Option", "Alt", "Shift"]; return modifiers.sort((a, b) => { return order.indexOf(a) - order.indexOf(b); }); }, unifyObject(hotkey: HotkeyType) { return { key: hotkey.key.toUpperCase(), modifiers: this.orderModifiers(hotkey.modifiers.map(modifier => StrUtil.ucFirst(modifier))), }; }, unifyString(hotkey: string): HotkeyType { const parts = hotkey.split("+"); const key = parts.pop() || ""; const modifiers: any[] = []; parts.forEach(part => { modifiers.push(StrUtil.ucFirst(part.trim())); }); return this.unifyObject({key, modifiers}); }, unify(hotkeys: string | string[] | HotkeyType | HotkeyType[]): HotkeyType[] { if (typeof hotkeys === "string") { return [this.unifyString(hotkeys)]; } else if (Array.isArray(hotkeys)) { return hotkeys.map(hotkey => { if (typeof hotkey === "string") { return this.unifyString(hotkey); } else { return this.unifyObject(hotkey); } }); } else { return [this.unifyObject(hotkeys)]; } }, getFromEvent(event: any): HotkeyType | null { const valid = [ "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "Space", ]; const key = (event.key || "").toUpperCase(); if (!event || !event.key || !valid.includes(key)) { return null; } const modifiers: HotkeyModifierType[] = []; if (isWin) { if (event.ctrlKey || event.control) { modifiers.push("Ctrl"); } if (event.altKey || event.alt) { modifiers.push("Alt"); } if (event.metaKey || event.meta) { modifiers.push("Win"); } } else if (isMac) { if (event.ctrlKey || event.control) { modifiers.push("Control"); } if (event.altKey || event.alt) { modifiers.push("Option"); } if (event.metaKey || event.meta) { modifiers.push("Command"); } } else { if (event.ctrlKey || event.control) { modifiers.push("Ctrl"); } if (event.altKey || event.alt) { modifiers.push("Alt"); } if (event.metaKey || event.meta) { modifiers.push("Meta"); } } if (event.shiftKey || event.shift) { modifiers.push("Shift"); } return this.unifyObject({key, modifiers}); }, match(hotkeysForMatch: HotkeyType[], hotkey: HotkeyType): boolean { if (!hotkeysForMatch || !hotkey) { return false; } const hotKeyStr = hotkey.modifiers.join("+") + "+" + hotkey.key; for (const key of hotkeysForMatch) { const keyStr = key.modifiers.join("+") + "+" + key.key; if (keyStr === hotKeyStr) { return true; } } return false; }, };