No damaged archive repair tool is magic. Be aware of these limitations:
Prevention is better than recovery. Follow these rules:
For an archive that is truly damaged (e.g., partially corrupted files, encoding errors), dart fix is not enough. Use it in tandem with: damaged archive repair tool dart fix
The Damaged Archive Repair Tool (DART) is a mediocre but sometimes lifesaving utility. It’s overpriced for what it does, and its feature set hasn’t evolved much in the last 5 years. However, when you have a single, non-encrypted, mildly corrupted ZIP or RAR file, and you don’t want to touch a command line, DART will probably work.
Recommendation:
Bottom line: 3.2 stars – it works when conditions are perfect, but don’t expect miracles. Keep backups next time.
Dart Fix is a simple, command-line repair utility designed to detect, analyze, and attempt automated recovery of common issues in archive files. It supports popular formats (ZIP, TAR, GZ, and RAR via optional support) and uses a stepwise strategy: No damaged archive repair tool is magic
Dart Fix emphasizes safety: it never overwrites the original file and produces a recovery report listing which files were fully recovered, partially recovered, or unrecoverable.
Dart Fix provides a focused, safe, and practical way to attempt recovery of damaged archives: it scans, salvages, and rebuilds while preserving the original file. Use it as part of a conservative recovery workflow: copy the original, run scans and salvage, verify recovered files, and maintain good backup and verification practices to reduce future risk. Bottom line: 3
If you want, I can:
(Reminder: do not run Dart Fix on the original file — always work with a copy.)
// damaged_archive_repair_tool.dart
import 'dart:io';
import 'dart:typed_data';
import 'dart:convert';
import 'package:archive/archive.dart';
/// Main tool for detecting and repairing damaged archive files
class DamagedArchiveRepairTool
final String archivePath;
late File _archiveFile;
late List<int> _originalBytes;
RepairStatistics statistics = RepairStatistics();
DamagedArchiveRepairTool(this.archivePath)
_archiveFile = File(archivePath);
/// Main entry point: analyze and attempt repair
Future<RepairResult> repair(bool aggressive = false) async
print('🔧 Damaged Archive Repair Tool');
print('📁 Processing: $archivePath');
// Step 1: Check if file exists
if (!await _archiveFile.exists())
return RepairResult.failure('Archive file not found');
// Step 2: Read file bytes
_originalBytes = await _archiveFile.readAsBytes();
statistics.originalSize = _originalBytes.length;
print('📦 Original size: $_formatSize(_originalBytes.length)');
// Step 3: Analyze damage
final damageReport = await _analyzeDamage();
print('\n🔍 Damage Analysis:');
print(damageReport);
// Step 4: Attempt repair based on damage type
List<int>? repairedBytes;
if (damageReport.hasCorruptCentralDirectory)
print('\n🛠️ Attempting central directory repair...');
repairedBytes = await _repairCentralDirectory();
if (damageReport.hasCorruptLocalHeaders && repairedBytes == null)
print('\n🛠️ Attempting local header repair...');
repairedBytes = await _repairLocalHeaders();
if (damageReport.hasTruncatedData && repairedBytes == null)
print('\n🛠️ Attempting truncation recovery...');
repairedBytes = await _recoverTruncatedData();
if (aggressive && repairedBytes == null)
print('\n⚠️ Aggressive recovery mode...');
repairedBytes = await _aggressiveRecovery();
// Step 5: Validate and save repair
if (repairedBytes != null && repairedBytes.isNotEmpty)
final isValid = await _validateArchive(repairedBytes);
if (isValid)
final repairedPath = _getRepairedPath();
await File(repairedPath).writeAsBytes(repairedBytes);
statistics.repairedSize = repairedBytes.length;
print('\n✅ Repair successful!');
print('📁 Repaired archive saved to: $repairedPath');
print('📊 Statistics:\n$statistics');
return RepairResult.success(repairedPath, statistics);
else
print('\n❌ Repair produced invalid archive');
return RepairResult.failure('Repair validation failed');
print('\n❌ Could not repair archive automatically');
return RepairResult.failure('No repair method succeeded');
/// Analyze archive to identify damage patterns
Future<DamageReport> _analyzeDamage() async
final report = DamageReport();
// Check for ZIP signature patterns
const localHeaderSig = [0x50, 0x4B, 0x03, 0x04];
const centralDirSig = [0x50, 0x4B, 0x01, 0x02];
const endOfCentralDirSig = [0x50, 0x4B, 0x05, 0x06];
// Find signatures
final localHeaders = _findSignatureIndices(localHeaderSig);
final centralDirs = _findSignatureIndices(centralDirSig);
final endCentralDirs = _findSignatureIndices(endOfCentralDirSig);
report.localHeaderCount = localHeaders.length;
report.centralDirectoryCount = centralDirs.length;
report.endCentralDirectoryCount = endCentralDirs.length;
// Check for truncation
if (endCentralDirs.isEmpty)
report.hasTruncatedData = true;
report.truncationPoint = _findLastValidStructure();
// Check for corrupt central directory
if (centralDirs.isNotEmpty && endCentralDirs.isEmpty)
report.hasCorruptCentralDirectory = true;
// Check for corrupt local headers
int corruptHeaders = 0;
for (final offset in localHeaders)
if (!_isValidLocalHeader(offset))
corruptHeaders++;
report.corruptLocalHeaderCount = corruptHeaders;
report.hasCorruptLocalHeaders = corruptHeaders > 0;
// Check for data corruption
report.hasDataCorruption = await _detectDataCorruption();
return report;
/// Repair corrupt central directory
Future<List<int>?> _repairCentralDirectory() async
try
print(' Rebuilding central directory from local headers...');
final localHeaders = await _extractLocalHeaders();
if (localHeaders.isEmpty) return null;
final rebuiltCentralDir = _rebuildCentralDirectory(localHeaders);
final repairedArchive = _replaceCentralDirectory(rebuiltCentralDir);
statistics.repairMethods.add('Central directory rebuild');
return repairedArchive;
catch (e)
print(' Central directory repair failed: $e');
return null;
/// Repair corrupt local file headers
Future<List<int>?> _repairLocalHeaders() async
try
print(' Attempting to repair individual local headers...');
final repairedBytes = List<int>.from(_originalBytes);
int repairs = 0;
// Scan for header patterns and fix common corruptions
for (int i = 0; i < repairedBytes.length - 4; i++) repairedBytes[i+2] == 0x01))
// Fix version and flag bytes if needed
if (repairedBytes[i+3] != 0x04)
repairedBytes[i+3] = 0x04;
repairs++;
if (repairs > 0)
statistics.repairMethods.add('Local header repair ($repairs fixes)');
return repairedBytes;
return null;
catch (e)
print(' Local header repair failed: $e');
return null;
/// Recover data from truncated archive
Future<List<int>?> _recoverTruncatedData() async
try
print(' Searching for recoverable data in truncated file...');
// Try to find complete entries even if central directory is missing
final entries = <ArchiveEntry>[];
int offset = 0;
while (offset < _originalBytes.length - 30)
final entry = _tryParseEntryAt(offset);
if (entry != null)
entries.add(entry);
offset += 30 + entry.compressedSize;
else
offset++;
if (entries.isNotEmpty)
print(' Recovered $entries.length entries');
final newArchive = _createArchiveFromEntries(entries);
statistics.repairMethods.add('Truncation recovery ($entries.length entries)');
return newArchive;
return null;
catch (e)
print(' Truncation recovery failed: $e');
return null;
/// Aggressive recovery - try to extract any readable data
Future<List<int>?> _aggressiveRecovery() async
print(' Scanning for any readable data patterns...');
final recoveredData = <int>[];
final Set<int> validBytes = 0x50, 0x4B, 0x03, 0x04, 0x50, 0x4B, 0x01, 0x02;
for (int i = 0; i < _originalBytes.length; i++)
if (recoveredData.length > _originalBytes.length * 0.5)
statistics.repairMethods.add('Aggressive byte-level recovery');
return recoveredData;
return null;
/// Validate if repaired archive is readable
Future<bool> _validateArchive(List<int> bytes) async
try
final archive = ZipDecoder().decodeBytes(bytes);
// Try to read first few entries to verify integrity
for (var i = 0; i < archive.files.length && i < 5; i++)
final file = archive.files[i];
if (file.isFile)
final content = file.content;
if (content.isEmpty && file.size > 0) return false;
statistics.validated = true;
return true;
catch (e)
print(' Validation failed: $e');
return false;
/// Helper: Find all occurrences of a byte signature
List<int> _findSignatureIndices(List<int> signature)
final indices = <int>[];
for (int i = 0; i <= _originalBytes.length - signature.length; i++)
bool match = true;
for (int j = 0; j < signature.length; j++)
if (_originalBytes[i + j] != signature[j])
match = false;
break;
if (match) indices.add(i);
return indices;
/// Helper: Check if local header at offset is valid
bool _isValidLocalHeader(int offset)
/// Helper: Find last valid structure before truncation
int _findLastValidStructure()
// Find last valid local file header
const sig = [0x50, 0x4B, 0x03, 0x04];
for (int i = _originalBytes.length - 4; i >= 0; i--)
bool match = true;
for (int j = 0; j < 4; j++)
if (_originalBytes[i + j] != sig[j])
match = false;
break;
if (match) return i;
return _originalBytes.length;
/// Helper: Detect data corruption using checksums
Future<bool> _detectDataCorruption() async
int crcErrors = 0;
int offset = 0;
while (offset < _originalBytes.length - 30)
if (_originalBytes[offset] == 0x50 &&
_originalBytes[offset+1] == 0x4B &&
_originalBytes[offset+2] == 0x03 &&
_originalBytes[offset+3] == 0x04)
// Read CRC from header
if (offset + 18 < _originalBytes.length)
final storedCrc = _readUint32(offset + 14);
// This is simplified - real CRC check would compute actual CRC
if (storedCrc == 0xFFFFFFFF
offset += 30;
else
offset++;
return crcErrors > 0;
/// Helper: Extract local headers from archive
Future<List<Map<String, dynamic>>> _extractLocalHeaders() async
final headers = <Map<String, dynamic>>[];
int offset = 0;
while (offset < _originalBytes.length - 30)
if (_originalBytes[offset] == 0x50 &&
_originalBytes[offset+1] == 0x4B &&
_originalBytes[offset+2] == 0x03 &&
_originalBytes[offset+3] == 0x04)
headers.add(
'offset': offset,
'filenameLength': _readUint16(offset + 26),
'extraLength': _readUint16(offset + 28),
'compressedSize': _readUint32(offset + 18),
);
offset += 30;
else
offset++;
return headers;
/// Helper: Rebuild central directory from local headers
List<int> _rebuildCentralDirectory(List<Map<String, dynamic>> headers)
// Simplified central directory rebuild
final buffer = <int>[];
// Write central directory signature
buffer.addAll([0x50, 0x4B, 0x01, 0x02]);
for (final header in headers)
// Write minimal central directory entry
buffer.addAll([0x14, 0x00]); // Version made by
buffer.addAll([0x14, 0x00]); // Version needed
buffer.addAll([0x00, 0x00]); // General purpose bit flag
buffer.addAll([0x08, 0x00]); // Compression method
buffer.addAll([0x00, 0x00, 0x00, 0x00]); // Mod time
buffer.addAll([0x00, 0x00, 0x00, 0x00]); // Mod date
buffer.addAll([0x00, 0x00, 0x00, 0x00]); // CRC-32
buffer.addAll(_toBytes(header['compressedSize'] as int, 4));
buffer.addAll(_toBytes(header['compressedSize'] as int, 4));
buffer.addAll(_toBytes(header['filenameLength'] as int, 2));
buffer.addAll(_toBytes(header['extraLength'] as int, 2));
buffer.addAll([0x00, 0x00]); // File comment length
buffer.addAll([0x00, 0x00]); // Disk number start
buffer.addAll([0x00, 0x00]); // Internal file attributes
buffer.addAll([0x00, 0x00, 0x00, 0x00]); // External file attributes
buffer.addAll(_toBytes(header['offset'] as int, 4));
// Placeholder for filename (would need actual name from original)
for (int i = 0; i < header['filenameLength']; i++)
buffer.add(0x20); // Space as placeholder
// Write end of central directory record
buffer.addAll([0x50, 0x4B, 0x05, 0x06]);
buffer.addAll([0x00, 0x00]); // Disk number
buffer.addAll([0x00, 0x00]); // Central dir disk
buffer.addAll(_toBytes(headers.length, 2));
buffer.addAll(_toBytes(headers.length, 2));
buffer.addAll(_toBytes(buffer.length - 22, 4));
buffer.addAll([0x00, 0x00, 0x00, 0x00]);
buffer.addAll([0x00, 0x00]);
return buffer;
/// Helper: Replace central directory in archive
List<int> _replaceCentralDirectory(List<int> newCentralDir)
final repaired = List<int>.from(_originalBytes);
// Find and remove old central directory
const endSig = [0x50, 0x4B, 0x05, 0x06];
for (int i = 0; i < repaired.length - 4; i++)
if (repaired[i] == endSig[0] &&
repaired[i+1] == endSig[1] &&
repaired[i+2] == endSig[2] &&
repaired[i+3] == endSig[3])
// Truncate at end of central directory
repaired.length = i;
break;
// Append new central directory
repaired.addAll(newCentralDir);
return repaired;
/// Helper: Parse archive entry at offset
ArchiveEntry? _tryParseEntryAt(int offset)
if (offset + 30 > _originalBytes.length) return null;
try
final filenameLength = _readUint16(offset + 26);
final extraLength = _readUint16(offset + 28);
final compressedSize = _readUint32(offset + 18);
if (filenameLength > 0 && filenameLength < 256 &&
compressedSize < 100 * 1024 * 1024)
// Likely a valid entry
return ArchiveEntry('recovered_$offset', compressedSize,
isFile: true,
compression: ArchiveCompression.STORE);
catch (e)
// Invalid entry
return null;
/// Helper: Create new archive from recovered entries
List<int> _createArchiveFromEntries(List<ArchiveEntry> entries)
final archive = Archive();
for (final entry in entries)
archive.addFile(entry);
return ZipEncoder().encode(archive)!;
/// Helper: Read Uint16 from bytes
int _readUint16(int offset) (_originalBytes[offset + 1] << 8);
/// Helper: Read Uint32 from bytes
int _readUint32(int offset)
(_originalBytes[offset + 1] << 8)
/// Helper: Convert integer to byte list
List<int> _toBytes(int value, int length)
final bytes = <int>[];
for (int i = 0; i < length; i++)
bytes.add((value >> (i * 8)) & 0xFF);
return bytes;
/// Helper: Format file size for display
String _formatSize(int bytes)
if (bytes < 1024) return '$bytes B';
if (bytes < 1024 * 1024) return '$(bytes / 1024).toStringAsFixed(1) KB';
return '$(bytes / (1024 * 1024)).toStringAsFixed(1) MB';
/// Helper: Generate repaired file path
String _getRepairedPath()
final file = File(archivePath);
final dir = file.parent.path;
final name = file.uri.pathSegments.last;
final extIndex = name.lastIndexOf('.');
if (extIndex > 0)
return '$dir/$name.substring(0, extIndex)_repaired$name.substring(extIndex)';
return '$dir/$name_repaired';
/// Damage analysis report
class DamageReport
int localHeaderCount = 0;
int centralDirectoryCount = 0;
int endCentralDirectoryCount = 0;
int corruptLocalHeaderCount = 0;
bool hasCorruptCentralDirectory = false;
bool hasCorruptLocalHeaders = false;
bool hasTruncatedData = false;
bool hasDataCorruption = false;
int truncationPoint = 0;
@override
String toString()
final buffer = StringBuffer();
buffer.writeln(' • Local headers found: $localHeaderCount');
buffer.writeln(' • Central directory entries: $centralDirectoryCount');
buffer.writeln(' • End of central directory: $endCentralDirectoryCount > 0 ? 'Present' : 'Missing'');
buffer.writeln(' • Corrupt local headers: $corruptLocalHeaderCount');
buffer.writeln(' • Data corruption detected: $hasDataCorruption');
buffer.writeln(' • Truncated archive: $hasTruncatedData');
return buffer.toString();
/// Repair statistics
class RepairStatistics
int originalSize = 0;
int repairedSize = 0;
int corruptBytesFixed = 0;
bool validated = false;
List<String> repairMethods = [];
@override
String toString()
return '''
Original size: $_formatSize(originalSize)
Repaired size: $_formatSize(repairedSize)
Bytes repaired: $corruptBytesFixed
Validation passed: $validated
Methods applied: $repairMethods.join(', ')''';
String _formatSize(int bytes)
if (bytes < 1024) return '$bytes B';
if (bytes < 1024 * 1024) return '$(bytes / 1024).toStringAsFixed(1) KB';
return '$(bytes / (1024 * 1024)).toStringAsFixed(1) MB';
/// Result of repair operation
class RepairResult
final bool success;
final String? repairedPath;
final RepairStatistics? statistics;
final String? error;
RepairResult._(this.success, this.repairedPath, this.statistics, this.error);
factory RepairResult.success(String path, RepairStatistics stats)
return RepairResult._(true, path, stats, null);
factory RepairResult.failure(String error)
return RepairResult._(false, null, null, error);
@override
String toString()
if (success)
return '✅ Success: Archive repaired to $repairedPath';
return '❌ Failed: $error';
/// Command-line interface
void main(List<String> args) async
print('═══════════════════════════════════════════');
print(' Damaged Archive Repair Tool v1.0');
print('═══════════════════════════════════════════\n');
if (args.isEmpty)
print('Usage: dart damaged_archive_repair_tool.dart <archive_path> [--aggressive]');
print('\nOptions:');
print(' --aggressive Enable aggressive recovery mode');
print('\nExamples:');
print(' dart repair.dart damaged.zip');
print(' dart repair.dart corrupted.zip --aggressive');
exit(1);
final archivePath = args[0];
final aggressive = args.contains('--aggressive');
final tool = DamagedArchiveRepairTool(archivePath);
try
final result = await tool.repair(aggressive: aggressive);
print('\n$result');
exit(result.success ? 0 : 1);
catch (e)
print('\n❌ Unexpected error: $e');
exit(1);