- name
- file-compression
- description
- Compress and decompress files using jsexec with built-in zlib and manual format parsing. Supports .gz, .tar, .tar.gz, .tgz, .zip.
- when_to_use
- Use this skill when the user needs to compress files into archives (.tar, .tar.gz, .tgz, .zip, .gz) or decompress/extract existing archives. **⚠️ Limitation: Directory compression is non-recursive — only top-level entries are archived; subdirectory contents are NOT included.**
- tools
- jsexec, read, write, bash
- args
- [{"name":"action","description":"Operation to perform — compress or decompress","required":true},{"name":"input","description":"Path to the input file or directory for compress, or the archive file for decompress","required":true},{"name":"output","description":"Output archive path (compress) or extraction directory (decompress). Auto-generated if omitted. For .gz decompress, this is the output TAR file path.","required":false},{"name":"format","description":"Target format for compression (tar, tar.gz, tgz, zip, gz). Auto-detected for decompress.","required":false}]
> **CODE SKILL — VERBATIM REQUIRED**
> This skill targets multiple archive formats (TAR, GZIP, ZIP) with manual binary parsing.
> The Runnable Entry Point is the sole implementation. Adaptation is prohibited per C2-A Rule 3.
## CRITICAL: Execution Constraints
The code below is the **SOLE IMPLEMENTATION** — it MUST NOT be replaced, simplified, or reimplemented.
**Prohibited deviations:**
- Do NOT write your own `crc32()` — use ONLY the one in Runnable Entry Point
- Do NOT write your own TAR header parser or builder — use ONLY the ones in Runnable Entry Point
- Do NOT write your own ZIP EOCD scanner or central directory parser — use ONLY the ones in Runnable Entry Point
- Do NOT skip any format-specific handling (.tar, .tar.gz, .tgz, .zip, .gz)
- Do NOT use shell tools (tar, gzip, unzip) — all operations are pure JavaScript in jsexec
- Do NOT alter the entry point dispatch logic (`action === 'compress'` / `action === 'decompress'`)
## Correct Invocation Pattern
1. Copy the **complete script** from `## Runnable Entry Point` below
2. Replace ONLY `{{action}}`, `{{input}}`, `{{output}}`, `{{format}}` with actual values
3. Since the IIFE is large (370+ lines), **persist to a temp file** and execute via `jsexec --file_path <file>` — do NOT embed the entire IIFE inside another JS string
4. Execute with **NO modifications** to core logic
## Argument Details
| Arg | Type | Required | Default | Description |
|-----|------|----------|---------|-------------|
| `action` | `string` | yes | — | `compress` or `decompress` |
| `input` | `string` | yes | — | Source file/directory (compress) or archive path (decompress) |
| `output` | `string` | no | auto | Output archive path (compress) or extract directory (decompress) |
| `format` | `string` | no | auto | `tar` / `tar.gz` / `tgz` / `zip` / `gz` (compress only). **When both `output` and `format` are provided, `format` takes priority.** When `output` is auto-generated, `format` determines the suffix. |
> **⚠️ `format` vs `output` extension**: When `format` is explicitly set, it overrides format detection from the `output` file extension. For example, `format=zip` + `output=archive.tar` still produces a valid ZIP. Without `format`, the extension determines the archive type.
## Runnable Entry Point (COMPLETE IIFE, DO NOT MODIFY)
```js
// ===== Runnable Entry Point (IIFE) — DO NOT MODIFY ANY CODE BELOW =====
(function() {
// ══════════════════════════════════════════════════════════════════════════
// Imports
// ══════════════════════════════════════════════════════════════════════════
const zlib = require('zlib');
const fs = require('fs');
const path = require('path');
// ══════════════════════════════════════════════════════════════════════════
// Utility Functions (DO NOT REPLACE)
// ══════════════════════════════════════════════════════════════════════════
function ensureDir(dir) {
try { fs.mkdirSync(dir); } catch (e) { }
}
function parseOctal(buf, start, end) {
let s = '';
for (let i = start; i < end; i++) {
const ch = String.fromCharCode(buf[i]);
if (ch >= '0' && ch <= '7') s += ch;
else break;
}
return parseInt(s, 8) || 0;
}
function crc32(buf) {
let crc = 0xFFFFFFFF;
const table = new Uint32Array(256);
for (let i = 0; i < 256; i++) {
let c = i;
for (let j = 0; j < 8; j++)
c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
table[i] = c;
}
for (let i = 0; i < buf.length; i++)
crc = table[(crc ^ buf[i]) & 0xFF] ^ (crc >>> 1);
return (crc ^ 0xFFFFFFFF) >>> 0;
}
// ══════════════════════════════════════════════════════════════════════════
// Core Logic — Decompress (DO NOT REPLACE)
// ══════════════════════════════════════════════════════════════════════════
function decompressGzip(inputPath, outputPath) {
const compressed = fs.readFileSync(inputPath);
const decompressed = zlib.gunzipSync(compressed);
fs.writeFileSync(outputPath, decompressed);
console.log(' ' + path.basename(outputPath) + ' (' + decompressed.length + ' bytes)');
}
function extractTar(tarPath, outputDir) {
const data = fs.readFileSync(tarPath);
ensureDir(outputDir);
let offset = 0, files = 0, dirs = 0;
while (offset + 512 <= data.length) {
const header = data.slice(offset, offset + 512);
if (header[0] === 0) break;
const name = header.slice(0, 100).toString().replace(/\0/g, '').trim();
const size = parseOctal(data, offset + 124, offset + 136);
const typeflag = header[156];
if (!name) break;
offset += 512;
const content = data.slice(offset, offset + size);
offset += Math.ceil(size / 512) * 512;
const fullPath = path.join(outputDir, name);
if (typeflag === 53) { ensureDir(fullPath); dirs++; }
else {
ensureDir(path.dirname(fullPath));
fs.writeFileSync(fullPath, content);
console.log(' ' + name + ' (' + size + ' bytes)');
files++;
}
}
console.log('Done: ' + files + ' files, ' + dirs + ' dirs');
}
function extractZip(zipPath, outputDir) {
const data = fs.readFileSync(zipPath);
ensureDir(outputDir);
let eocdOffset = -1;
for (let i = data.length - 22; i >= 0; i--) {
if (data[i] === 0x50 && data[i + 1] === 0x4B &&
data[i + 2] === 0x05 && data[i + 3] === 0x06) {
eocdOffset = i; break;
}
}
if (eocdOffset === -1) throw new Error('EOCD not found');
const totalEntries = data.readUInt16LE(eocdOffset + 8);
const cdOffset = data.readUInt32LE(eocdOffset + 16);
let cdPos = cdOffset;
let count = 0, dirCount = 0;
for (let i = 0; i < totalEntries; i++) {
const sig = data.readUInt32LE(cdPos);
if (sig !== 0x02014B50) throw new Error('Corrupt central directory');
const compMethod = data.readUInt16LE(cdPos + 10);
const compSize = data.readUInt32LE(cdPos + 20);
const nameLen = data.readUInt16LE(cdPos + 28);
const extraLen = data.readUInt16LE(cdPos + 30);
const localOff = data.readUInt32LE(cdPos + 42);
const fileName = data.slice(cdPos + 46, cdPos + 46 + nameLen).toString();
cdPos += 46 + nameLen + extraLen;
const localNameLen = data.readUInt16LE(localOff + 26);
const localExtraLen = data.readUInt16LE(localOff + 28);
const dataStart = localOff + 30 + localNameLen + localExtraLen;
const fileData = data.slice(dataStart, dataStart + compSize);
const fullPath = path.join(outputDir, fileName);
if (fileName.endsWith('/')) { ensureDir(fullPath); dirCount++; continue; }
ensureDir(path.dirname(fullPath));
let decompressed;
if (compMethod === 0) decompressed = fileData;
else if (compMethod === 8) decompressed = zlib.inflateRawSync(fileData);
else { continue; }
fs.writeFileSync(fullPath, decompressed);
console.log(' ' + fileName + ' (' + decompressed.length + ' bytes)');
count++;
}
console.log('Done: ' + count + ' files, ' + dirCount + ' dirs');
}
function decompress(inputPath, outputDir) {
try { fs.statSync(inputPath); } catch (e) {
console.error('Error: File not found - ' + inputPath); return;
}
const basename = path.basename(inputPath);
if (!outputDir) {
const name = basename.replace(/\.tar\.gz$/i, '').replace(/\.tgz$/i, '')
.replace(/\.tar$/i, '').replace(/\.gz$/i, '').replace(/\.zip$/i, '');
outputDir = path.join(path.dirname(inputPath), name + '_out');
}
console.log('Operation: Decompress');
console.log('Source: ' + basename);
console.log('Output: ' + outputDir);
if (basename.endsWith('.tar.gz') || basename.endsWith('.tgz')) {
const tmp = '/tmp/_t' + Date.now() + '.tar';
fs.writeFileSync(tmp, zlib.gunzipSync(fs.readFileSync(inputPath)));
extractTar(tmp, outputDir);
try { fs.rmSync(tmp); } catch (e) { }
} else if (basename.endsWith('.tar')) {
extractTar(inputPath, outputDir);
} else if (basename.endsWith('.gz')) {
const gzOut = outputDir || path.join(path.dirname(inputPath), basename.replace(/\.gz$/i, ''));
decompressGzip(inputPath, gzOut);
} else if (basename.endsWith('.zip')) {
extractZip(inputPath, outputDir);
} else {
console.error('Error: Unsupported format - ' + basename);
}
}
// ══════════════════════════════════════════════════════════════════════════
// Core Logic — Compress (DO NOT REPLACE)
// ══════════════════════════════════════════════════════════════════════════
function buildTarGz(files, baseDir, outputPath) {
const blocks = [];
for (const entry of files) {
const relPath = typeof entry === 'string' ? entry : entry.name;
const isDir = typeof entry === 'object' ? entry.isDir : false;
const fullPath = path.join(baseDir, relPath);
const header = Buffer.alloc(512, 0);
header.write(relPath, 0, 100, 'utf8');
if (isDir) {
header.write('0000755', 100, 8, 'utf8');
header.write('0000000', 108, 8, 'utf8');
header.write('0000000', 116, 8, 'utf8');
header[156] = 53;
blocks.push(header);
console.log(' Added: ' + relPath + '/ (dir)');
continue;
}
const content = fs.readFileSync(fullPath);
header.write('0000644', 100, 8, 'utf8');
header.write('0000000', 108, 8, 'utf8');
header.write('0000000', 116, 8, 'utf8');
const sizeStr = content.length.toString(8).padStart(11, '0');
for (let i = 0; i < 11; i++) header[124 + i] = sizeStr.charCodeAt(i);
header[135] = 32;
const timeStr = Math.floor(Date.now() / 1000).toString(8).padStart(11, '0');
for (let i = 0; i < 11; i++) header[136 + i] = timeStr.charCodeAt(i);
header[147] = 32;
header[156] = 48;
header.write('ustar', 257, 5, 'utf8');
header[262] = 0; header[263] = 48; header[264] = 48;
let checksum = 0;
for (let i = 0; i < 512; i++) checksum += header[i];
const chkStr = checksum.toString(8).padStart(6, '0');
for (let i = 0; i < 6; i++) header[148 + i] = chkStr.charCodeAt(i);
header[154] = 0; header[155] = 32;
blocks.push(header);
blocks.push(content);
const pad = (512 - content.length % 512) % 512;
if (pad > 0) blocks.push(Buffer.alloc(pad, 0));
console.log(' Added: ' + relPath + ' (' + content.length + ' bytes)');
}
blocks.push(Buffer.alloc(512, 0));
blocks.push(Buffer.alloc(512, 0));
const tarData = Buffer.concat(blocks);
let finalData;
let ext = path.extname(outputPath).toLowerCase();
if (ext === '.gz' || ext === '.tgz' || outputPath.endsWith('.tar.gz')) {
finalData = zlib.gzipSync(tarData);
} else {
finalData = tarData;
}
fs.writeFileSync(outputPath, finalData);
console.log('Created: ' + path.basename(outputPath) + ' (' + finalData.length + ' bytes)');
const ratio = finalData.length > 0 ? (100 * (1 - finalData.length / tarData.length)).toFixed(1) : '0.0';
console.log('Compression ratio: ' + ratio + '%');
}
Auf GitHub ansehen