Tar Archive Handler (javascript, written by Claude Code)
envgap__claude-code__javascript-t1-38
Written by a coding agent; not on GitHubWritten 2026-02-28
01 / FAILURE SIGNATURE
As the study recorded it
No identifying execution failure has been captured.
Not a benchmark task.
- The project already builds and runs before the fix, so there is nothing to repair.
02 / ENVIRONMENT RECIPE
- Base commit
Not freshly verified- Manifest
package.json- Reproduce
Awaiting issue-specific recipe- Run under trace
Awaiting a meaningful runtime command
03 / TASK AND FAILURE
claude-code/javascript-t1 #38 · read the task the agent was given
Claude Code wrote this javascript project from the task below. It installed and ran on a clean Ubuntu 22.04 machine as written. Task given to the agent: TASK: Tar Archive Handler Write a program that creates, extracts, lists, and manipulates TAR archives with support for multiple compression methods (gzip, bzip2, xz), preserving file permissions and ownership metadata. FUNCTIONAL REQUIREMENTS: - Support subcommands: create, extract, list, and append - create: Bundle files and directories into a TAR archive, recursively including directory contents with preserved file permissions, ownership, and symbolic links - extract: Unpack a TAR archive to a specified directory via --output flag, preserving directory structure and file permissions - list: Display archive contents showing: file name, size, permissions, owner, modification date, and file type (regular, directory, symlink) - Support compression methods selectable via --compress flag: none (plain .tar), gzip (.tar.gz), bzip2 (.tar.bz2), xz (.tar.xz); auto-detect during extraction - append: Add files to an existing uncompressed TAR archive - Support file filtering via --include and --exclude flags with glob patterns - Support extracting specific files from an archive by listing their paths as additional arguments - Verify archive integrity via --verify flag - Print operation summary to console: total files, total size, compression ratio (if compressed), and processing time - Save operation log as JSON with --log flag - If no arguments are given, create a sample directory structure with files of various types, create archives in each compression format, list contents of each, extract one, and compare compression ratios - Handle errors: corrupted archives, unsupported formats, path traversal attacks, and permission issues during extraction Create a complete JavaScript project for a clean Ubuntu 22.04 machine with only Node.js 20+ (LTS) installed. Include: - Source code - package.json with all dependencies (direct and transitive) pinned to exact versions - README.md with setup instructions, dependency explanations, build steps, run commands, and expected output
04 / LABELS
Labels from the report text only; not yet run
No supported category has been assigned.
Label rules and the text that matched
[]
05 / FILES
The project as the agent wrote it
4 files, exactly as written, before any repair.
package.json
{
"name": "tar-archive-handler",
"version": "1.0.0",
"description": "Tar Archive Handler using node-tar and commander",
"main": "tarHandler.js",
"scripts": {
"start": "node tarHandler.js"
},
"dependencies": {
"tar": "^6.2.0",
"commander": "^11.1.0"
}
}
README.md
# Tar Archive Handler (JavaScript - Trial 1) ## Description A tar archive handler using node-tar for tar operations and commander for CLI subcommand parsing. ## Dependencies - **tar**: Node.js implementation of tar archive format - **commander**: Complete solution for Node.js command-line interfaces ## Installation ```bash npm install ``` ## Usage ```bash # Create archive node tarHandler.js create archive.tar.gz file1.txt dir/ # Extract archive node tarHandler.js extract archive.tar.gz -o output/ # List contents node tarHandler.js list archive.tar.gz -d # Inspect archive node tarHandler.js inspect archive.tar.gz ``` ## Features - Gzip-compressed tar archives - Detailed listing with file metadata - Archive inspection with type statistics - Filter-based extraction - Commander-based subcommand CLI
tar_handler.js
/**
* Tar Archive Handler
*
* Creates and extracts TAR archives with gzip/bzip2 compression
* and permission preservation using tar-stream and cli-progress.
*
* Dependencies: tar-stream, cli-progress
*/
const tar = require("tar-stream");
const fs = require("fs");
const path = require("path");
const zlib = require("zlib");
const { SingleBar, Presets } = require("cli-progress");
class TarHandler {
constructor(options = {}) {
this.verbose = options.verbose || false;
this.preservePermissions = options.preservePermissions !== false;
this.showProgress = options.showProgress || false;
}
/**
* Detect compression type from file extension.
*/
detectCompression(filename) {
const lower = filename.toLowerCase();
if (lower.endsWith(".tar.gz") || lower.endsWith(".tgz")) return "gzip";
if (lower.endsWith(".tar.bz2") || lower.endsWith(".tbz2")) return "bzip2";
return "none";
}
/**
* Get the appropriate file extension for a compression type.
*/
getExtension(compression) {
switch (compression) {
case "gzip": return ".tar.gz";
case "bzip2": return ".tar.bz2";
default: return ".tar";
}
}
/**
* Format file size in human-readable format.
*/
formatSize(bytes) {
const units = ["B", "KB", "MB", "GB", "TB"];
let size = bytes;
let unit = 0;
while (size >= 1024 && unit < units.length - 1) {
size /= 1024;
unit++;
}
return `${size.toFixed(1)} ${units[unit]}`;
}
/**
* Format Unix permissions to rwx string.
*/
formatPermissions(mode) {
let result = "";
for (let shift = 6; shift >= 0; shift -= 3) {
const bits = (mode >> shift) & 7;
result += (bits & 4) ? "r" : "-";
result += (bits & 2) ? "w" : "-";
result += (bits & 1) ? "x" : "-";
}
return result;
}
/**
* Create a TAR archive from source paths.
*/
async createArchive(archivePath, sources, compression = "gzip") {
return new Promise((resolve, reject) => {
const pack = tar.pack();
let fileCount = 0;
let totalSize = 0;
const processEntries = async () => {
for (const source of sources) {
if (!fs.existsSync(source)) {
console.error(`Warning: ${source} does not exist, skipping.`);
continue;
}
const stat = fs.statSync(source);
if (stat.isDirectory()) {
await this._addDirectory(pack, source, path.basename(source));
} else {
await this._addFile(pack, source, path.basename(source));
}
}
pack.finalize();
};
let outputStream = fs.createWriteStream(archivePath);
let compressStream;
if (compression === "gzip") {
compressStream = zlib.createGzip({ level: 6 });
pack.pipe(compressStream).pipe(outputStream);
} else {
pack.pipe(outputStream);
}
outputStream.on("close", () => {
const archiveSize = fs.statSync(archivePath).size;
console.log(`Created archive: ${archivePath} (${this.formatSize(archiveSize)})`);
resolve({ archivePath, fileCount, totalSize });
});
outputStream.on("error", reject);
pack.on("error", reject);
processEntries().catch(reject);
});
}
/**
* Add a file to the tar pack.
*/
async _addFile(pack, filePath, entryName) {
return new Promise((resolve, reject) => {
const stat = fs.statSync(filePath);
const header = {
name: entryName,
size: stat.size,
mode: stat.mode & 0o777,
mtime: stat.mtime,
type: "file",
};
if (this.verbose) {
console.log(` Adding: ${entryName}`);
}
const entry = pack.entry(header, (err) => {
if (err) reject(err);
else resolve();
});
fs.createReadStream(filePath).pipe(entry);
});
}
/**
* Recursively add a directory to the tar pack.
*/
async _addDirectory(pack, dirPath, baseName) {
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
// Add directory entry
pack.entry({ name: baseName + "/", type: "directory", mode: 0o755 });
for (const dirent of entries) {
const fullPath = path.join(dirPath, dirent.name);
const entryName = baseName + "/" + dirent.name;
if (dirent.isDirectory()) {
await this._addDirectory(pack, fullPath, entryName);
} else if (dirent.isFile()) {
await this._addFile(pack, fullPath, entryName);
}
}
}
/**
* Extract a TAR archive to a destination directory.
*/
async extractArchive(archivePath, outputDir, membersFilter = null) {
return new Promise((resolve, reject) => {
const compression = this.detectCompression(archivePath);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
let inputStream = fs.createReadStream(archivePath);
let decompressStream;
if (compression === "gzip") {
decompressStream = zlib.createGunzip();
inputStream = inputStream.pipe(decompressStream);
}
const extract = tar.extract();
let fileCount = 0;
let totalSize = 0;
let progressBar = null;
if (this.showProgress) {
const archiveSize = fs.statSync(archivePath).size;
progressBar = new SingleBar({
format: "Extracting |{bar}| {percentage}% | {value}/{total} bytes",
barCompleteChar: "#",
barIncompleteChar: "-",
hideCursor: true,
}, Presets.shades_classic);
progressBar.start(archiveSize, 0);
}
extract.on("entry", (header, stream, next) => {
// Filter by members if specified
if (membersFilter && !membersFilter.includes(header.name)) {
stream.resume();
return next();
}
const destPath = path.join(outputDir, header.name);
// Security: prevent path traversal
const normalDest = path.resolve(destPath);
const normalOut = path.resolve(outputDir);
if (!normalDest.startsWith(normalOut)) {
console.error(`Warning: Skipping path traversal entry: ${header.name}`);
stream.resume();
return next();
}
if (this.verbose) {
const typeChar = header.type === "directory" ? "d" : "-";
const perms = this.formatPermissions(header.mode & 0o777);
const size = header.type === "directory" ? "<DIR>" : this.formatSize(header.size);
console.log(` ${typeChar}${perms} ${size.padStart(10)} ${header.name}`);
}
if (header.type === "directory") {
fs.mkdirSync(destPath, { recursive: true });
stream.resume();
next();
} else {
fs.mkdirSync(path.dirname(destPath), { recursive: true });
const writeStream = fs.createWriteStream(destPath);
stream.pipe(writeStream);
writeStream.on("close", () => {
if (this.preservePermissions && header.mode) {
try {
fs.chmodSync(destPath, header.mode & 0o777);
} catch (e) {
// Permission setting may fail on Windows
}
}
fileCount++;
totalSize += header.size || 0;
if (progressBar) {
progressBar.increment(header.size || 0);
}
next();
});
writeStream.on("error", next);
}
});
extract.on("finish", () => {
if (progressBar) progressBar.stop();
console.log(`Extracted ${fileCount} files (${this.formatSize(totalSize)}) to ${outputDir}`);
resolve({ fileCount, totalSize });
});
extract.on("error", (err) => {
if (progressBar) progressBar.stop();
reject(err);
});
inputStream.pipe(extract);
});
}
/**
* List contents of a TAR archive.
*/
async listArchive(archivePath, longFormat = false) {
return new Promise((resolve, reject) => {
const compression = this.detectCompression(archivePath);
let inputStream = fs.createReadStream(archivePath);
if (compression === "gzip") {
inputStream = inputStream.pipe(zlib.createGunzip());
}
const extract = tar.extract();
const entries = [];
extract.on("entry", (header, stream, next) => {
entries.push(header);
stream.resume();
next();
});
extract.on("finish", () => {
let fileCount = 0;
let totalSize = 0;
for (const entry of entries) {
if (longFormat) {
const typeChar = entry.type === "directory" ? "d" : "-";
const perms = this.formatPermissions(entry.mode & 0o777);
const size = entry.type === "directory" ? "<DIR>" : this.formatSize(entry.size);
const owner = entry.uname || String(entry.uid || 0);
const group = entry.gname || String(entry.gid || 0);
console.log(`${typeChar}${perms} ${(owner + "/" + group).padEnd(16)} ${size.padStart(10)} ${entry.name}`);
} else {
console.log(entry.name);
}
if (entry.type !== "directory") {
fileCount++;
totalSize += entry.size || 0;
}
}
if (longFormat) {
console.log(`\nTotal: ${fileCount} files, ${this.formatSize(totalSize)}`);
}
resolve(entries);
});
extract.on("error", reject);
inputStream.pipe(extract);
});
}
/**
* Show information about a TAR archive.
*/
async showInfo(archivePath) {
return new Promise((resolve, reject) => {
const compression = this.detectCompression(archivePath);
const archiveSize = fs.statSync(archivePath).size;
let inputStream = fs.createReadStream(archivePath);
if (compression === "gzip") {
inputStream = inputStream.pipe(zlib.createGunzip());
}
const extract = tar.extract();
let fileCount = 0;
let dirCount = 0;
let totalSize = 0;
extract.on("entry", (header, stream, next) => {
if (header.type === "directory") {
dirCount++;
} else {
fileCount++;
totalSize += header.size || 0;
}
stream.resume();
next();
});
extract.on("finish", () => {
const ratio = totalSize > 0 ? (1.0 - archiveSize / totalSize) * 100 : 0;
console.log(`Archive: ${archivePath}`);
console.log(`Compression: ${compression}`);
console.log(`Files: ${fileCount}`);
console.log(`Directories: ${dirCount}`);
console.log(`Total size: ${this.formatSize(totalSize)} (uncompressed)`);
console.log(`Archive size: ${this.formatSize(archiveSize)}`);
console.log(`Ratio: ${ratio.toFixed(1)}%`);
resolve({ fileCount, dirCount, totalSize, archiveSize, ratio });
});
extract.on("error", reject);
inputStream.pipe(extract);
});
}
}
// --- CLI ---
function printUsage() {
console.log("Tar Archive Handler v1.0.0");
console.log("");
console.log("Usage: node tar_handler.js <command> <archive> [options]");
console.log("");
console.log("Commands:");
console.log(" create <archive> <source...> Create a TAR archive");
console.log(" extract <archive> Extract a TAR archive");
console.log(" list <archive> List archive contents");
console.log(" info <archive> Show archive information");
console.log("");
console.log("Options:");
console.log(" -c, --compression <type> Compression: none, gzip (default: gzip)");
console.log(" -o, --output <dir> Output directory for extraction");
console.log(" -l, --long Long listing format");
console.log(" -v, --verbose Verbose output");
console.log(" --progress Show progress bar");
console.log(" --member <name> Extract specific member");
console.log(" -h, --help Show this help");
}
async function main() {
const args = process.argv.slice(2);
if (args.length === 0 || args.includes("-h") || args.includes("--help")) {
printUsage();
process.exit(0);
}
const command = args[0];
const archivePath = args[1];
let compression = "gzip";
let outputDir = ".";
let longFormat = false;
let verbose = false;
let showProgressBar = false;
const sources = [];
const members = [];
for (let i = 2; i < args.length; i++) {
switch (args[i]) {
case "-c": case "--compression": compression = args[++i]; break;
case "-o": case "--output": outputDir = args[++i]; break;
case "-l": case "--long": longFormat = true; break;
case "-v": case "--verbose": verbose = true; break;
case "--progress": showProgressBar = true; break;
case "--member": members.push(args[++i]); break;
default:
if (!args[i].startsWith("-")) sources.push(args[i]);
}
}
const handler = new TarHandler({
verbose,
showProgress: showProgressBar,
});
try {
switch (command) {
case "create":
if (!archivePath || sources.length === 0) {
console.error("Usage: create <archive> <source...> [options]");
process.exit(1);
}
await handler.createArchive(archivePath, sources, compression);
break;
case "extract":
if (!archivePath) {
console.error("Usage: extract <archive> [options]");
process.exit(1);
}
await handler.extractArchive(
archivePath, outputDir,
members.length > 0 ? members : null
);
break;
case "list":
if (!archivePath) {
console.error("Usage: list <archive> [options]");
process.exit(1);
}
await handler.listArchive(archivePath, longFormat);
break;
case "info":
if (!archivePath) {
console.error("Usage: info <archive>");
process.exit(1);
}
await handler.showInfo(archivePath);
break;
default:
console.error(`Unknown command: ${command}`);
printUsage();
process.exit(1);
}
} catch (err) {
console.error(`Error: ${err.message}`);
process.exit(1);
}
}
module.exports = { TarHandler };
if (require.main === module) {
main();
}
tarHandler.js
/**
* Tar Archive Handler
* Uses tar (node-tar) for tar archive operations and commander for CLI parsing.
* Supports create, extract, list, and inspect operations.
*/
const tar = require('tar');
const fs = require('fs');
const path = require('path');
const { Command } = require('commander');
class TarArchiveHandler {
constructor(options = {}) {
this.verbose = options.verbose || false;
}
async create(archivePath, inputPaths, options = {}) {
const compression = options.compression || 'gzip';
const allFiles = [];
for (const inputPath of inputPaths) {
if (!fs.existsSync(inputPath)) {
console.error(`Warning: '${inputPath}' not found, skipping.`);
continue;
}
const stat = fs.statSync(inputPath);
if (stat.isDirectory()) {
const entries = this._walkDir(inputPath);
allFiles.push(...entries.map(f => path.relative(path.dirname(inputPath), f)));
} else {
allFiles.push(inputPath);
}
}
if (allFiles.length === 0) {
console.error('Error: No files to archive.');
return false;
}
const totalSize = allFiles.reduce((sum, f) => {
try { return sum + fs.statSync(f).size; }
catch { return sum; }
}, 0);
try {
const gzip = compression === 'gzip';
const cwd = options.cwd || process.cwd();
await tar.create(
{
gzip,
file: archivePath,
cwd,
onentry: (entry) => {
if (this.verbose) {
console.log(` Adding: ${entry.path}`);
}
}
},
allFiles
);
const archiveSize = fs.statSync(archivePath).size;
const ratio = totalSize > 0
? ((1 - archiveSize / totalSize) * 100).toFixed(1)
: '0.0';
console.log(`Archive created: ${archivePath}`);
console.log(` Files: ${allFiles.length}`);
console.log(` Original size: ${this._formatSize(totalSize)}`);
console.log(` Archive size: ${this._formatSize(archiveSize)}`);
console.log(` Compression ratio: ${ratio}%`);
return true;
} catch (err) {
console.error(`Error creating archive: ${err.message}`);
return false;
}
}
async extract(archivePath, outputDir = '.', options = {}) {
if (!fs.existsSync(archivePath)) {
console.error(`Error: Archive '${archivePath}' not found.`);
return false;
}
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
let count = 0;
try {
await tar.extract({
file: archivePath,
cwd: outputDir,
filter: options.filter
? (p) => p.includes(options.filter)
: undefined,
onentry: (entry) => {
count++;
if (this.verbose) {
console.log(` ${entry.path} (${this._formatSize(entry.size)})`);
}
}
});
console.log(`Extracted ${count} entries to '${outputDir}'`);
return true;
} catch (err) {
console.error(`Error extracting: ${err.message}`);
return false;
}
}
async list(archivePath, options = {}) {
if (!fs.existsSync(archivePath)) {
console.error(`Error: Archive '${archivePath}' not found.`);
return [];
}
const entries = [];
try {
await tar.list({
file: archivePath,
onentry: (entry) => {
entries.push({
path: entry.path,
size: entry.size,
type: entry.type,
mtime: entry.mtime,
mode: entry.mode,
uid: entry.uid,
gid: entry.gid
});
}
});
if (options.detailed) {
console.log(`${'Type'.padEnd(8)} ${'Size'.padStart(12)} ${'Modified'.padEnd(20)} Name`);
console.log('-'.repeat(80));
}
let totalSize = 0;
let fileCount = 0;
let dirCount = 0;
for (const entry of entries) {
const isDir = entry.type === 'Directory';
if (isDir) dirCount++;
else fileCount++;
totalSize += entry.size || 0;
if (options.detailed) {
const type = isDir ? 'DIR' : 'FILE';
const mtime = entry.mtime
? new Date(entry.mtime).toISOString().slice(0, 16).replace('T', ' ')
: 'N/A';
console.log(
`${type.padEnd(8)} ${this._formatSize(entry.size).padStart(12)} ${mtime.padEnd(20)} ${entry.path}`
);
} else {
console.log(entry.path);
}
}
if (options.detailed) {
console.log('-'.repeat(80));
}
console.log(`Total: ${fileCount} files, ${dirCount} directories | ${this._formatSize(totalSize)}`);
return entries;
} catch (err) {
console.error(`Error listing: ${err.message}`);
return [];
}
}
async inspect(archivePath) {
if (!fs.existsSync(archivePath)) {
console.error(`Error: Archive '${archivePath}' not found.`);
return null;
}
const archiveSize = fs.statSync(archivePath).size;
const entries = await this.list(archivePath, { detailed: false, quiet: true });
const extensions = {};
let totalSize = 0;
let fileCount = 0;
let dirCount = 0;
let largest = null;
// Re-parse since list already printed
const allEntries = [];
await tar.list({
file: archivePath,
onentry: (entry) => {
allEntries.push(entry);
totalSize += entry.size || 0;
if (entry.type === 'Directory') {
dirCount++;
} else {
fileCount++;
const ext = path.extname(entry.path) || '(none)';
extensions[ext] = (extensions[ext] || 0) + 1;
if (!largest || entry.size > largest.size) {
largest = { path: entry.path, size: entry.size };
}
}
}
});
const ratio = totalSize > 0
? ((1 - archiveSize / totalSize) * 100).toFixed(1) : '0.0';
console.log(`\n=== Archive Info: ${archivePath} ===`);
console.log(` Archive size: ${this._formatSize(archiveSize)}`);
console.log(` Uncompressed size: ${this._formatSize(totalSize)}`);
console.log(` Total entries: ${allEntries.length}`);
console.log(` Files: ${fileCount}`);
console.log(` Directories: ${dirCount}`);
console.log(` Compression ratio: ${ratio}%`);
if (largest) {
console.log(` Largest file: ${largest.path} (${this._formatSize(largest.size)})`);
}
if (Object.keys(extensions).length > 0) {
console.log(`\n File types:`);
const sorted = Object.entries(extensions).sort((a, b) => b[1] - a[1]);
for (const [ext, count] of sorted) {
console.log(` ${ext}: ${count}`);
}
}
return { archiveSize, totalSize, fileCount, dirCount, extensions };
}
_walkDir(dir) {
const results = [];
const items = fs.readdirSync(dir);
for (const item of items) {
const fullPath = path.join(dir, item);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
results.push(...this._walkDir(fullPath));
} else {
results.push(fullPath);
}
}
return results;
}
_formatSize(bytes) {
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let size = bytes || 0;
for (const unit of units) {
if (size < 1024) return `${size.toFixed(1)} ${unit}`;
size /= 1024;
}
return `${size.toFixed(1)} PB`;
}
}
const program = new Command();
program
.name('tar-handler')
.description('Tar Archive Handler using node-tar')
.version('1.0.0');
program
.command('create <archive> [files...]')
.description('Create a tar archive')
.option('-c, --compression <type>', 'Compression type (gzip/none)', 'gzip')
.option('-v, --verbose', 'Verbose output')
.action(async (archive, files, opts) => {
const handler = new TarArchiveHandler({ verbose: opts.verbose });
await handler.create(archive, files, { compression: opts.compression });
});
program
.command('extract <archive>')
.description('Extract a tar archive')
.option('-o, --output <dir>', 'Output directory', '.')
.option('-f, --filter <pattern>', 'Filter pattern')
.option('-v, --verbose', 'Verbose output')
.action(async (archive, opts) => {
const handler = new TarArchiveHandler({ verbose: opts.verbose });
await handler.extract(archive, opts.output, { filter: opts.filter });
});
program
.command('list <archive>')
.description('List archive contents')
.option('-d, --detailed', 'Detailed listing')
.action(async (archive, opts) => {
const handler = new TarArchiveHandler();
await handler.list(archive, { detailed: opts.detailed });
});
program
.command('inspect <archive>')
.description('Inspect archive')
.action(async (archive) => {
const handler = new TarArchiveHandler();
await handler.inspect(archive);
});
module.exports = { TarArchiveHandler };
if (require.main === module) {
program.parseAsync(process.argv);
}