Skip to content

Instantly share code, notes, and snippets.

@krtx
Created July 29, 2019 13:16
Show Gist options
  • Save krtx/cf219ccdbc3bd6423145f4ae6e0578c5 to your computer and use it in GitHub Desktop.
Save krtx/cf219ccdbc3bd6423145f4ae6e0578c5 to your computer and use it in GitHub Desktop.
import 'dart:io';
import 'dart:convert';
import 'package:args/args.dart';
// read bytes into integer in little endian
getBytes(List<int> bytes, int offset, int length) {
int res = 0;
for (var i = 0; i < length; i++) {
res += bytes[i + offset] << (i * 8);
}
return res;
}
class CentralDirectoryHeader {
var versionMadeBy;
var versionExtract;
var bitFlag;
var compressionMethod;
DateTime lastModified;
var crc32;
var compressedSize;
var uncompressedSize;
var diskNumber;
var internalFileAtrributes;
var externalFileAtrributes;
var localHeaderOffset;
var fileName;
var extraField;
var comment;
decode(List<int> bytes, int offset) {
assert(bytes[offset] == 0x50 &&
bytes[offset + 1] == 0x4b &&
bytes[offset + 2] == 0x01 &&
bytes[offset + 3] == 0x02);
this.versionMadeBy = getBytes(bytes, offset + 4, 2);
this.versionExtract = getBytes(bytes, offset + 6, 2);
this.bitFlag = getBytes(bytes, offset + 8, 2);
this.compressionMethod = getBytes(bytes, offset + 10, 2);
var b12 = getBytes(bytes, offset + 12, 2);
var hour = b12 >> 11;
var minute = (b12 >> 5) & 63;
var second = (b12 & 31) * 2;
var b14 = getBytes(bytes, offset + 14, 2);
var year = (b14 >> 9) + 1980;
var month = (b14 >> 5) & 15;
var day = b14 & 31;
this.lastModified = DateTime(year, month, day, hour, minute, second);
this.crc32 = getBytes(bytes, offset + 16, 4);
this.compressedSize = getBytes(bytes, offset + 20, 4);
this.uncompressedSize = getBytes(bytes, offset + 24, 4);
var fileNameLength = getBytes(bytes, offset + 28, 2);
var extraFieldLength = getBytes(bytes, offset + 30, 2);
var fileCommentLength = getBytes(bytes, offset + 32, 2);
this.diskNumber = getBytes(bytes, offset + 34, 2);
this.internalFileAtrributes = getBytes(bytes, offset + 36, 2);
this.externalFileAtrributes = getBytes(bytes, offset + 38, 4);
this.localHeaderOffset = getBytes(bytes, offset + 42, 4);
this.fileName = Utf8Decoder().convert(bytes
.getRange(
offset + 46,
offset + 46 + fileNameLength,
)
.toList(growable: false));
this.extraField = bytes
.getRange(
offset + 46 + fileNameLength,
offset + 46 + fileNameLength + extraFieldLength,
)
.toList(growable: false);
this.comment = Utf8Decoder().convert(bytes
.getRange(
offset + 46 + fileNameLength + extraFieldLength,
offset + 46 + fileNameLength + extraFieldLength + fileCommentLength,
)
.toList(growable: false));
return offset + 46 + fileNameLength + extraFieldLength + fileCommentLength;
}
String toString() {
return '''
versionMadeBy = ${this.versionMadeBy}
versionExtract = ${this.versionExtract}
bitFlag = ${this.bitFlag}
compressionMethod = ${this.compressionMethod}
lastModified = ${this.lastModified}
crc32 = ${this.crc32}
compressedSize = ${this.compressedSize}
uncompressedSize = ${this.uncompressedSize}
diskNumber = ${this.diskNumber}
internalFileAttributes = ${this.internalFileAtrributes}
externalFileAttributes = ${this.externalFileAtrributes}
localHeaderOffset = ${this.localHeaderOffset}
fileName = "${this.fileName}"
comment = "${this.comment}"''';
}
}
class EndOfCentralDirectoryRecord {
var diskNumber;
var firstDiskNumber;
var filesInDisk;
var files;
var totalHeaderSize;
var headerOffset;
var comment;
// シグニチャを探してそこからdecodeする
decode(List<int> bytes) {
// 後ろから探す
for (var i = bytes.length - 4; i >= 0; i--) {
if (bytes[i] == 0x50 &&
bytes[i + 1] == 0x4b &&
bytes[i + 2] == 0x05 &&
bytes[i + 3] == 0x06) {
this.diskNumber = getBytes(bytes, i + 4, 2);
this.firstDiskNumber = getBytes(bytes, i + 6, 2);
this.filesInDisk = getBytes(bytes, i + 8, 2);
this.files = getBytes(bytes, i + 10, 2);
this.totalHeaderSize = getBytes(bytes, i + 12, 4);
this.headerOffset = getBytes(bytes, i + 16, 4);
var commentLength = getBytes(bytes, i + 20, 2);
this.comment = Utf8Decoder()
.convert(bytes.getRange(i + 22, i + 22 + commentLength).toList());
return;
}
}
throw FormatException('end of central directory record is not found');
}
String toString() {
return '''
diskNumber = ${diskNumber}
firstDiskNumber = ${firstDiskNumber}
filesInDisk = ${filesInDisk}
files = ${files}
totalHeaderSize = ${totalHeaderSize}
headerOffset = ${headerOffset}
comment = "${comment}"''';
}
}
main(List<String> arguments) async {
var parser = ArgParser();
parser.addOption('dir', abbr: 'd', defaultsTo: '');
var results = parser.parse(arguments);
var targetDir = results['dir'];
if (results.rest.isEmpty) {
print('filename is required');
exit(1);
}
var filename = results.rest[0];
var bytes = await File(filename).readAsBytes();
var eocd = EndOfCentralDirectoryRecord();
eocd.decode(bytes);
List<CentralDirectoryHeader> headers = [];
var offset = eocd.headerOffset;
for (var i = 0; i < eocd.files; i++) {
var header = CentralDirectoryHeader();
offset = header.decode(bytes, offset);
headers.add(header);
}
for (var header in headers) {
var fileNameLength = getBytes(bytes, header.localHeaderOffset + 26, 2);
var extraFieldLength = getBytes(bytes, header.localHeaderOffset + 28, 2);
var rawBytes = bytes
.getRange(
header.localHeaderOffset + 30 + fileNameLength + extraFieldLength,
header.localHeaderOffset +
30 +
fileNameLength +
extraFieldLength +
header.compressedSize)
.toList();
var data = rawBytes;
if (header.compressionMethod == 8) {
var inflator = RawZLibFilter.inflateFilter(raw: true);
inflator.process(data, 0, data.length);
data = inflator.processed(end: true);
}
if (targetDir != '') {
Directory(targetDir).createSync(recursive: true);
}
if (header.fileName.endsWith('/')) {
Directory('${targetDir}/${header.fileName}').createSync(recursive: true);
} else {
File('${targetDir}/${header.fileName}').writeAsBytesSync(data);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment