diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..755c365b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,17 @@ +{ + "name": "node-nodejs-fundamentals", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "node-nodejs-fundamentals", + "version": "1.0.0", + "license": "ISC", + "engines": { + "node": ">=24.10.0", + "npm": ">=10.9.2" + } + } + } +} diff --git a/src/cli/interactive.js b/src/cli/interactive.js index d0e3e0d9..2348a07e 100644 --- a/src/cli/interactive.js +++ b/src/cli/interactive.js @@ -1,8 +1,51 @@ +import readline from 'readline'; + const interactive = () => { - // Write your code here - // Use readline module for interactive CLI - // Support commands: uptime, cwd, date, exit - // Handle Ctrl+C and unknown commands + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + rl.setPrompt('> '); + rl.prompt(); + rl.on('line', (input) => { + const command = input.trim(); + + switch (command) { + case 'uptime': + console.log('Uptime:', process.uptime(), 's'); + break; + + case 'cwd': + console.log(process.cwd()); + break; + + case 'date': + console.log(new Date().toISOString()); + break; + + case 'exit': + rl.close(); + return; + + case '': + break; + + default: + console.log('Unknown command'); + } + + rl.prompt(); + }); + + rl.on('SIGINT', () => { + rl.close(); + }); + + rl.on('close', () => { + console.log('Goodbye!'); + process.exit(0); +}); }; -interactive(); +interactive(); \ No newline at end of file diff --git a/src/cli/progress.js b/src/cli/progress.js index 3e060763..5c4c9428 100644 --- a/src/cli/progress.js +++ b/src/cli/progress.js @@ -1,8 +1,69 @@ const progress = () => { - // Write your code here - // Simulate progress bar from 0% to 100% over ~5 seconds - // Update in place using \r every 100ms - // Format: [████████████████████ ] 67% + + const args = process.argv.slice(2); + + let duration = 5000; + let interval = 100; + let length = 30; + let percent = 0; + let color = null; + let colorCode = null; + + const colorIndex = args.indexOf('--color'); + if (colorIndex !== -1) { + color = args[colorIndex + 1]; + } + if (color && !/^#[0-9A-Fa-f]{6}$/.test(color)) { + color = null; + } + if (color) { + const r = parseInt(color.slice(1, 3), 16); + const g = parseInt(color.slice(3, 5), 16); + const b = parseInt(color.slice(5, 7), 16); + + colorCode = `\x1b[38;2;${r};${g};${b}m`; + } + + const durationIndex = args.indexOf('--duration'); + if (durationIndex !== -1) { + duration = Number(args[durationIndex + 1]); + } + + const intervalIndex = args.indexOf('--interval'); + if (intervalIndex !== -1) { + interval = Number(args[intervalIndex + 1]); + } + + + const lengthIndex = args.indexOf('--length'); + if (lengthIndex !== -1) { + length = Number(args[lengthIndex + 1]); + } + + const steps = Math.ceil(duration / interval); + const increment = 100 / steps; + + + const timer = setInterval(() => { + percent += increment; + if (percent > 100) percent = 100; + + const filled = Math.floor((length * percent) / 100); + const empty = length - filled; + const filledPart = '█'.repeat(filled); + const emptyPart = ' '.repeat(empty); + const coloredFilled = colorCode ? `${colorCode}${filledPart}\x1b[0m`: filledPart; + const bar = coloredFilled + emptyPart; + + process.stdout.write(`\r[${bar}] ${Math.floor(percent)}%`); + + if (percent >= 100) { + clearInterval(timer); + process.stdout.write('\nDone!\n'); + } + }, interval); }; progress(); + +// Temporary comment \ No newline at end of file diff --git a/src/cp/execCommand.js b/src/cp/execCommand.js index 34a89c8d..d90bc666 100644 --- a/src/cp/execCommand.js +++ b/src/cp/execCommand.js @@ -1,10 +1,19 @@ +import { spawn } from 'node:child_process'; + const execCommand = () => { - // Write your code here - // Take command from CLI argument - // Spawn child process - // Pipe child stdout/stderr to parent stdout/stderr - // Pass environment variables - // Exit with same code as child + const command = process.argv[2]; + + const child = spawn(command, { + shell: true, + env: process.env + }); + + child.stdout.pipe(process.stdout); + child.stderr.pipe(process.stderr); + + child.on('close', (code) => { + process.exit(code); + }); }; execCommand(); diff --git a/src/fs/findByExt.js b/src/fs/findByExt.js index 24f06cb8..7ea1f5a0 100644 --- a/src/fs/findByExt.js +++ b/src/fs/findByExt.js @@ -1,7 +1,63 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; + const findByExt = async () => { - // Write your code here - // Recursively find all files with specific extension - // Parse --ext CLI argument (default: .txt) + + const index = process.argv.indexOf('--ext'); + const ext = process.argv[index + 1]; + let extension; + + if (index !== -1) { + extension = ext.startsWith('.') ? ext : '.' + ext; + } else { + extension = '.txt'; + } + + const workspacePath = path.resolve('src/workspace'); + + try { + const stats = await fs.stat(workspacePath); + + if (!stats.isDirectory()) { + throw new Error('FS operation failed'); + } + let result = [] + await walk(workspacePath, workspacePath, extension, result); + + result.sort(); + for (const file of result) { + console.log(file); + } + + } catch (err) { + if (err.code === 'ENOENT') { + throw new Error('FS operation failed'); + } + throw err; + } + + async function walk(currentPath, basePath, extension, result) { + const entries = await fs.readdir(currentPath, { withFileTypes: true }); + + for (const dirent of entries) { + + const fullPath = path.join(currentPath, dirent.name); + const relativePath = path.relative(basePath, fullPath); + + if (dirent.isDirectory()) { + await walk(fullPath, basePath, extension, result); + + } else if (dirent.isFile()) { + if (path.extname(dirent.name) === extension){ + result.push(relativePath) + } + } + } + } }; -await findByExt(); + + + + +await findByExt().catch(console.error); \ No newline at end of file diff --git a/src/fs/merge.js b/src/fs/merge.js index cb8e0d8f..3c801836 100644 --- a/src/fs/merge.js +++ b/src/fs/merge.js @@ -1,8 +1,64 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; + const merge = async () => { - // Write your code here - // Default: read all .txt files from workspace/parts in alphabetical order - // Optional: support --files filename1,filename2,... to merge specific files in provided order - // Concatenate content and write to workspace/merged.txt + +const index = process.argv.indexOf('--files') + +let files; +if (index !== -1) { + files = process.argv[index + 1].split(','); +} + +const workspacePath = path.resolve('src/workspace/parts'); + +try{ + const stats = await fs.stat(workspacePath); + + if (!stats.isDirectory()) { + throw new Error('FS operation failed'); + } + if(!files){ + const entries = await fs.readdir(workspacePath, { withFileTypes: true }); + + files = []; + for (const dirent of entries) { + if(dirent.isFile() && path.extname(dirent.name) === '.txt'){ + files.push(dirent.name) + } + } + files.sort() + + if(files.length === 0){ + throw new Error('FS operation failed'); + } + } else { + if(files.length === 0 || (files.length === 1 && files[0] === '')){ + throw new Error('FS operation failed'); + } + for (const file of files){ + try { + await fs.access(path.join(workspacePath, file)); + } catch { + throw new Error('FS operation failed'); + } + } + } + let mergedContent = '' + for (const file of files){ + const content = await fs.readFile(path.join(workspacePath, file), 'utf8') + mergedContent += content + } + await fs.writeFile(path.resolve('src/workspace', 'merged.txt'), mergedContent) + + +} catch (err) { + if (err.code === 'ENOENT') { + throw new Error('FS operation failed'); + } + throw err; +} + }; -await merge(); +await merge().catch(console.error); \ No newline at end of file diff --git a/src/fs/restore.js b/src/fs/restore.js index 96ae1ffb..d22e5929 100644 --- a/src/fs/restore.js +++ b/src/fs/restore.js @@ -1,8 +1,50 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; + + const restore = async () => { - // Write your code here - // Read snapshot.json - // Treat snapshot.rootPath as metadata only - // Recreate directory/file structure in workspace_restored -}; + const snapshotPath = path.resolve('src/snapshot.json'); + const workspaceRestoredPath = path.resolve('src/workspace_restored'); + + try{ + await fs.stat(snapshotPath); + } + + catch(err){ + if (err.code === 'ENOENT') { + throw new Error('FS operation failed'); + } + throw err; + }; + + try { + await fs.stat(workspaceRestoredPath); + throw new Error('FS operation failed'); + } catch (err) { + if (err.code === 'ENOENT') { + await fs.mkdir(workspaceRestoredPath); + } else { + throw err; + } + } + + const data = await fs.readFile(snapshotPath); + const snapshot = JSON.parse(data); + + for(const entry of snapshot.entries){ + if(entry.type === 'directory'){ + await fs.mkdir(path.join(workspaceRestoredPath, entry.path), { recursive: true }); + } + else if (entry.type === 'file'){ + const targetPath = path.join(workspaceRestoredPath, entry.path); + await fs.mkdir(path.dirname(targetPath), { recursive: true }); + const buffer = Buffer.from(entry.content, 'base64'); + await fs.writeFile(targetPath, buffer); + } + } +} + +restore().catch(console.error); + + -await restore(); diff --git a/src/fs/snapshot.js b/src/fs/snapshot.js index 050103d3..cfd5a55c 100644 --- a/src/fs/snapshot.js +++ b/src/fs/snapshot.js @@ -1,9 +1,66 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; + const snapshot = async () => { - // Write your code here - // Recursively scan workspace directory - // Write snapshot.json with: - // - rootPath: absolute path to workspace - // - entries: flat array of relative paths and metadata -}; - -await snapshot(); + const workspacePath = path.resolve('src/workspace'); + + try { + const stats = await fs.stat(workspacePath) + + if(!stats.isDirectory()){ + throw new Error('FS operation failed'); + } + + let result = [] + + await walk(workspacePath, workspacePath, result) + async function walk(currentPath, basePath, result) { + + const entries = await fs.readdir(currentPath, { withFileTypes: true }); + + for (const dirent of entries){ + const fullPath = path.join(currentPath, dirent.name); + const relativePath = path.relative(basePath, fullPath); + const normalizedPath = relativePath.split(path.sep).join('/'); + + if(dirent.isDirectory()){ + result.push( + { path: normalizedPath, type: 'directory' }) + await walk(fullPath, basePath, result); + } + + else if (dirent.isFile()) { + const fileStats = await fs.stat(fullPath) + const content = await fs.readFile(fullPath) + const base64 = content.toString('base64'); + result.push( + { + path: normalizedPath, + type: 'file', + size: fileStats.size, + content: base64 + } + ) + } + } +} + + const snapshotData = { + rootPath: workspacePath, + entries: result + }; + + const snapshotPath = path.join(path.dirname(workspacePath), 'snapshot.json'); + await fs.writeFile(snapshotPath, JSON.stringify(snapshotData, null, 2)); + + } + catch (err) { + if (err.code === 'ENOENT') { + throw new Error('FS operation failed'); + } + throw err; + } + +} + +snapshot().catch(console.error); diff --git a/src/hash/file1.txt b/src/hash/file1.txt new file mode 100644 index 00000000..b6fc4c62 --- /dev/null +++ b/src/hash/file1.txt @@ -0,0 +1 @@ +hello \ No newline at end of file diff --git a/src/hash/file2.txt b/src/hash/file2.txt new file mode 100644 index 00000000..04fea064 --- /dev/null +++ b/src/hash/file2.txt @@ -0,0 +1 @@ +world \ No newline at end of file diff --git a/src/hash/verify.js b/src/hash/verify.js index 7f1e8961..9dd60d8c 100644 --- a/src/hash/verify.js +++ b/src/hash/verify.js @@ -1,8 +1,46 @@ +import { readFile } from "fs/promises"; +import { fileURLToPath } from "url"; +import { dirname, join } from "path"; +import { createReadStream } from "fs"; +import { createHash } from "crypto"; + const verify = async () => { - // Write your code here - // Read checksums.json - // Calculate SHA256 hash using Streams API - // Print result: filename — OK/FAIL + const __filename = fileURLToPath(import.meta.url); + const __dirname = dirname(__filename); + + let data; + + try { + data = await readFile(join(__dirname, "checksums.json"), "utf8"); + } catch { + throw new Error("FS operation failed"); + } + + const checksums = JSON.parse(data); + const entries = Object.entries(checksums); + + for (const [filename, expectedHash] of entries) { + const actualHash = await new Promise((resolve, reject) => { + const hash = createHash("sha256"); + const stream = createReadStream(join(__dirname, filename)); + + stream.on("data", (chunk) => { + hash.update(chunk); + }); + + stream.on("end", () => { + resolve(hash.digest("hex")); + }); + + stream.on("error", reject); + }); + + if (actualHash === expectedHash) { + console.log(`${filename} — OK`); + } else { + console.log(`${filename} — FAIL`); + } + } }; -await verify(); +await verify(); \ No newline at end of file diff --git a/src/modules/dynamic.js b/src/modules/dynamic.js index 008ca387..7431a58c 100644 --- a/src/modules/dynamic.js +++ b/src/modules/dynamic.js @@ -1,9 +1,16 @@ -const dynamic = async () => { - // Write your code here - // Accept plugin name as CLI argument - // Dynamically import plugin from plugins/ directory - // Call run() function and print result - // Handle missing plugin case +const pluginName = process.argv[process.argv.length - 1]; + +const dynamic = async (pluginName) => { +const pluginPath = `./plugins/${pluginName}.js`; + try { + const plugin = await import(pluginPath); + + const result = plugin.run(); + console.log(result); + } catch (err) { + console.log("Plugin not found"); + process.exit(1); + } }; -await dynamic(); +await dynamic(pluginName); diff --git a/src/streams/filter.js b/src/streams/filter.js index 3868ab46..dd49e189 100644 --- a/src/streams/filter.js +++ b/src/streams/filter.js @@ -1,9 +1,38 @@ +import { Transform } from 'node:stream'; + const filter = () => { - // Write your code here - // Read from process.stdin - // Filter lines by --pattern CLI argument - // Use Transform Stream - // Write to process.stdout + +const args = process.argv; +const patternIndex = args.indexOf('--pattern'); +const pattern = patternIndex !== -1 ? args[patternIndex + 1] : ''; + +let remainder = ''; + +const t = new Transform({ + transform(chunk, enc, cb) { + const text = remainder + chunk.toString(); + const lines = text.split('\n'); + remainder = lines.pop(); + + const filtered = lines.filter((l) => { + return l.includes(pattern); + }); + + cb(null, filtered.join('\n') + '\n'); + }, + + _flush(cb) { + if (remainder.includes(pattern)) { + this.push(remainder + '\n'); + } + cb(); +} +}); + + + +process.stdin.pipe(t).pipe(process.stdout); }; + filter(); diff --git a/src/streams/lineNumberer.js b/src/streams/lineNumberer.js index 579d662e..655d3116 100644 --- a/src/streams/lineNumberer.js +++ b/src/streams/lineNumberer.js @@ -1,8 +1,35 @@ +import { Transform, pipeline } from 'node:stream'; + const lineNumberer = () => { - // Write your code here - // Read from process.stdin - // Use Transform Stream to prepend line numbers - // Write to process.stdout + let line = 1; + let remainder = ''; + + const t = new Transform({ + transform(chunk, enc, cb) { + const text = remainder + chunk.toString(); + const lines = text.split('\n'); + remainder = lines.pop(); + + const numbered = lines.map((l) => { + const res = `${line} | ${l}`; + line++; + return res; + }); + cb(null, numbered.join('\n') + '\n'); + }, + + _flush(cb) { + if (remainder) { + cb(null, `${line} | ${remainder}\n`); + } else { + cb(); + } + } + }); + + pipeline(process.stdin, t, process.stdout, (err) => { + if (err) process.exit(1); + }); }; -lineNumberer(); +lineNumberer(); \ No newline at end of file diff --git a/src/streams/split.js b/src/streams/split.js index f8f814fa..7d64f048 100644 --- a/src/streams/split.js +++ b/src/streams/split.js @@ -1,8 +1,70 @@ -const split = async () => { - // Write your code here - // Read source.txt using Readable Stream - // Split into chunk_1.txt, chunk_2.txt, etc. - // Each chunk max N lines (--lines CLI argument, default: 10) +import { createReadStream, createWriteStream } from 'node:fs'; +import { Transform } from 'node:stream'; + +const split = () => { + const args = process.argv; + const linesIndex = args.indexOf('--lines'); + const maxLines = linesIndex !== -1 ? Number(args[linesIndex + 1]) : 10; + + let remainder = ''; + let lineCount = 0; + let fileIndex = 1; + let ws = null; + + + const t = new Transform({ + transform(chunk, enc, cb) { + const text = remainder + chunk.toString(); + const lines = text.split('\n'); + + remainder = lines.pop(); + + for (const line of lines) { + if (!ws) { + ws = createWriteStream(`chunk_${fileIndex}.txt`); + } + + ws.write(line + '\n'); + lineCount++; + + if (lineCount === maxLines) { + ws.end(); + fileIndex++; + ws = null; + lineCount = 0; + } + } + + cb(); + }, + + _flush(cb) { + if (remainder) { + if (!ws) { + ws = createWriteStream(`chunk_${fileIndex}.txt`) + } + + ws.write(remainder + '\n'); + lineCount++; + + if (lineCount === maxLines) { + ws.end(); + fileIndex++; + ws = null; + lineCount = 0; + } + } + + if (ws) { + ws.end(); + } + + cb(); + } + }); + const rs = createReadStream('source.txt'); + + rs.pipe(t); }; -await split(); +split(); diff --git a/src/wt/main.js b/src/wt/main.js index d7d21f0c..0bd0d0fa 100644 --- a/src/wt/main.js +++ b/src/wt/main.js @@ -1,11 +1,54 @@ +import os from 'node:os' +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url' +import { Worker } from 'node:worker_threads' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const dataPath = path.join(__dirname, 'data.json') + const main = async () => { - // Write your code here - // Read data.json containing array of numbers - // Split into N chunks (N = CPU cores) - // Create N workers, send one chunk to each - // Collect sorted chunks - // Merge using k-way merge algorithm - // Log final sorted array + const raw = await fs.readFile(dataPath, 'utf8') + const data = JSON.parse(raw) + const numCPUs =os.cpus().length + const chunkSize = Math.ceil(data.length / numCPUs) + + const chunks = [] + + for (let i = 0; i < data.length; i += chunkSize){ + chunks.push(data.slice(i, i + chunkSize)) + } + + const workerPath = path.join(__dirname, 'worker.js') + + const results = await Promise.all( + chunks.map((chunk, index) => new Promise((resolve, reject) => { + const worker = new Worker(workerPath, { workerData: chunk }); + worker.on('message', resolve); + worker.on('error', reject); + })) + ); + + const merged = []; + const arrays = results.map(arr => [...arr]); + + while (arrays.some(arr => arr.length > 0)) { + let minIndex = -1; + let minValue = Infinity; + for (let i = 0; i < arrays.length; i++) { + if (arrays[i].length === 0) continue; + const value = arrays[i][0]; + if (value < minValue) { + minValue = value; + minIndex = i; + } + } + merged.push(minValue); + arrays[minIndex].shift(); + } + + console.log(merged); }; -await main(); +await main(); \ No newline at end of file diff --git a/src/wt/worker.js b/src/wt/worker.js index 15f42fc8..64786950 100644 --- a/src/wt/worker.js +++ b/src/wt/worker.js @@ -1,9 +1,4 @@ -import { parentPort } from 'worker_threads'; +import { workerData, parentPort } from 'node:worker_threads' -// Receive array from main thread -// Sort in ascending order -// Send back to main thread - -parentPort.on('message', (data) => { - // Write your code here -}); +const sorted = [...workerData].sort((a, b) => a - b) +parentPort.postMessage(sorted) diff --git a/src/zip/compressDir.js b/src/zip/compressDir.js index 3a3c5089..9eb95778 100644 --- a/src/zip/compressDir.js +++ b/src/zip/compressDir.js @@ -1,9 +1,92 @@ +import { readdir, access, mkdir, stat } from 'fs/promises' +import { createReadStream, createWriteStream } from 'fs' +import { createBrotliCompress } from 'zlib' +import path from 'path' + +const rootDir = path.join(process.cwd(), 'workspace', 'toCompress') +const compressedDir = path.join(process.cwd(), 'workspace', 'compressed') +const archivePath = path.join(compressedDir, 'archive.br') + +// Binary framing format per entry: +// [4 bytes: path length][path bytes][1 byte: type (0=file,1=dir)][8 bytes: content length][content bytes] +// For directories content length is 0. + +const collectEntries = async (dir, base) => { + const entries = [] + const items = await readdir(dir, { withFileTypes: true }) + for (const item of items) { + const fullPath = path.join(dir, item.name) + const relativePath = path.relative(base, fullPath).split(path.sep).join('/') + if (item.isDirectory()) { + entries.push({ type: 'dir', relativePath }) + const nested = await collectEntries(fullPath, base) + entries.push(...nested) + } else if (item.isFile()) { + const fileStats = await stat(fullPath) + entries.push({ type: 'file', relativePath, fullPath, size: fileStats.size }) + } + } + return entries +} + +const writeUInt32BE = (n) => { + const buf = Buffer.allocUnsafe(4) + buf.writeUInt32BE(n, 0) + return buf +} + +const writeBigUInt64BE = (n) => { + const buf = Buffer.allocUnsafe(8) + buf.writeBigUInt64BE(BigInt(n), 0) + return buf +} + const compressDir = async () => { - // Write your code here - // Read all files from workspace/toCompress/ - // Compress entire directory structure into archive.br - // Save to workspace/compressed/ - // Use Streams API -}; - -await compressDir(); + try { + await access(rootDir) + } catch { + throw new Error('FS operation failed') + } + + await mkdir(compressedDir, { recursive: true }) + + const entries = await collectEntries(rootDir, rootDir) + + await new Promise((resolve, reject) => { + const writeStream = createWriteStream(archivePath) + const brotli = createBrotliCompress() + + brotli.pipe(writeStream) + writeStream.on('finish', resolve) + writeStream.on('error', reject) + brotli.on('error', reject) + + const processEntries = async () => { + for (const entry of entries) { + const pathBuf = Buffer.from(entry.relativePath, 'utf8') + brotli.write(writeUInt32BE(pathBuf.length)) + brotli.write(pathBuf) + + if (entry.type === 'dir') { + brotli.write(Buffer.from([1])) + brotli.write(writeBigUInt64BE(0)) + } else { + brotli.write(Buffer.from([0])) + brotli.write(writeBigUInt64BE(entry.size)) + + await new Promise((res, rej) => { + const rs = createReadStream(entry.fullPath) + rs.on('data', chunk => brotli.write(chunk)) + rs.on('end', res) + rs.on('error', rej) + }) + } + } + brotli.end() + } + + processEntries().catch(reject) + }) +} + +await compressDir() \ No newline at end of file diff --git a/src/zip/decompressDir.js b/src/zip/decompressDir.js index d6e770f6..bbcf6e5e 100644 --- a/src/zip/decompressDir.js +++ b/src/zip/decompressDir.js @@ -1,8 +1,82 @@ +import { access, mkdir, writeFile } from 'fs/promises' +import { createReadStream } from 'fs' +import { createBrotliDecompress } from 'zlib' +import path from 'path' + +const compressedDir = path.join(process.cwd(), 'workspace', 'compressed') +const archivePath = path.join(compressedDir, 'archive.br') +const decompressedDir = path.join(process.cwd(), 'workspace', 'decompressed') + +const readBytes = (stream, n) => new Promise((resolve, reject) => { + const tryRead = () => { + const chunk = stream.read(n) + if (chunk !== null) { + resolve(chunk) + } else { + stream.once('readable', tryRead) + stream.once('error', reject) + stream.once('end', () => resolve(null)) + } + } + tryRead() +}) + const decompressDir = async () => { - // Write your code here - // Read archive.br from workspace/compressed/ - // Decompress and extract to workspace/decompressed/ - // Use Streams API -}; + try { + await access(compressedDir) + await access(archivePath) + } catch { + throw new Error('FS operation failed') + } + + await mkdir(decompressedDir, { recursive: true }) + + const brotli = createBrotliDecompress() + createReadStream(archivePath).pipe(brotli) + + await new Promise((resolve) => brotli.once('readable', resolve)) + + while (true) { + const pathLenBuf = await readBytes(brotli, 4) + if (pathLenBuf === null || pathLenBuf.length < 4) break + + const pathLen = pathLenBuf.readUInt32BE(0) + + const pathBuf = await readBytes(brotli, pathLen) + if (pathBuf === null) break + const relativePath = pathBuf.toString('utf8') + + const typeBuf = await readBytes(brotli, 1) + if (typeBuf === null) break + const isDir = typeBuf[0] === 1 + + const sizeBuf = await readBytes(brotli, 8) + if (sizeBuf === null) break + const contentSize = Number(sizeBuf.readBigUInt64BE(0)) + + const destPath = path.join(decompressedDir, relativePath) + + if (isDir) { + await mkdir(destPath, { recursive: true }) + } else { + await mkdir(path.dirname(destPath), { recursive: true }) + + if (contentSize === 0) { + await writeFile(destPath, Buffer.alloc(0)) + } else { + let remaining = contentSize + const chunks = [] + while (remaining > 0) { + const toRead = Math.min(remaining, 65536) + const chunk = await readBytes(brotli, toRead) + if (chunk === null) break + chunks.push(chunk) + remaining -= chunk.length + } + await writeFile(destPath, Buffer.concat(chunks)) + } + } + } +} -await decompressDir(); +await decompressDir() \ No newline at end of file