From 5325bfbc902e9a0ff7b42e3fbee2c40dfa61b998 Mon Sep 17 00:00:00 2001 From: Kai Sellgren Date: Wed, 26 Sep 2012 20:38:09 +0300 Subject: [PATCH] Major improvements to creating archives. --- lib/Util.dart | 28 +++- lib/Zip.dart | 158 +++++++++++------ lib/central_directory.dart | 30 ++-- lib/central_directory_file_header.dart | 114 +++++++++---- lib/end_of_central_directory_record.dart | 48 ++++-- lib/local_file_header.dart | 82 +++++---- lib/util.dart | 52 ++++++ lib/zip.dart | 205 +++++++++++++++++++++++ test/test.dart | 8 +- 9 files changed, 574 insertions(+), 151 deletions(-) create mode 100644 lib/util.dart create mode 100644 lib/zip.dart diff --git a/lib/Util.dart b/lib/Util.dart index 5065be9..ab17b6b 100644 --- a/lib/Util.dart +++ b/lib/Util.dart @@ -5,7 +5,7 @@ import 'dart:math'; /** * Converts the byte sequence to a numeric representation. */ -bytesToValue(List bytes) { +int bytesToValue(List bytes) { var value = 0; for (var i = 0, length = bytes.length; i < length; i++) { @@ -15,10 +15,34 @@ bytesToValue(List bytes) { return value; } +/** + * Converts the given value to a byte sequence. + * + * The parameter [minByteCount] specifies how many bytes should be returned at least. + */ +List valueToBytes(int value, [int minByteCount = 0]) { + var bytes = [0x00, 0x00, 0x00, 0x00]; + + if (value == null) value = 0; + + var i = 0; + var actualByteCount = 0; + do { + bytes[i++] = value & (255); + value = value >> 8; + actualByteCount += 1; + + if (value == 0) + break; + } while (i < 4); + + return bytes.getRange(0, max(minByteCount, actualByteCount)); +} + /** * Returns true if the two given lists are equal. */ -bool listsAreEqual(List one, List two) { +bool listsAreEqual(final List one, final List two) { var i = -1; return one.every((element) { i++; diff --git a/lib/Zip.dart b/lib/Zip.dart index 77fefc7..e176aa2 100644 --- a/lib/Zip.dart +++ b/lib/Zip.dart @@ -6,12 +6,15 @@ * http://www.opensource.org/licenses/mit-license.php */ -library Zip; +library zip; import 'dart:io'; import 'end_of_central_directory_record.dart'; import 'central_directory.dart'; +import 'central_directory_file_header.dart'; +import 'local_file_header.dart'; import 'util.dart'; +import 'package:crc32/crc32.dart'; /** * This class represents a Zip file. @@ -25,31 +28,79 @@ class Zip { Path _filePath; File _file; - List _data; - EndOfCentralDirectoryRecord _endOfCentralDirectoryRecord; - CentralDirectory _centralDirectory; - List files; - bool _initialized = false; + EndOfCentralDirectoryRecord _endOfCentralDirectoryRecord = new EndOfCentralDirectoryRecord(); + CentralDirectory _centralDirectory = new CentralDirectory(); - Zip(Path this._filePath); + Zip(path) { + if (path is String) + _filePath = new Path(path); + else + _filePath = path; + } + + /** + * Saves the Zip archive. + */ + void save() { + _file = new File.fromPath(this._filePath); + + _file.create().then((File file) { + file.open(FileMode.WRITE).then((RandomAccessFile raf) { + + // Start by writing Local File Headers. + _centralDirectory.fileHeaders.forEach((CentralDirectoryFileHeader cdfh) { + + // Save the current position on the Central Directory File Header. + cdfh.localHeaderOffset = raf.positionSync(); + + // Save the header and write it to the file. + final buffer = cdfh.localFileHeader.save(); + raf.writeListSync(buffer, 0, buffer.length); + }); + + // We are now at the location of the Central Directory. + final centralDirectoryOffset = raf.positionSync(); + + // Continue with the Central Directory Record. + _centralDirectory.fileHeaders.forEach((CentralDirectoryFileHeader cdfh) { + + // Save the header and write it to the file. + final buffer = cdfh.save(); + raf.writeListSync(buffer, 0, buffer.length); + }); + + // Last, write the End of Central Directory Record. + _endOfCentralDirectoryRecord.centralDirectoryOffset = centralDirectoryOffset; + _endOfCentralDirectoryRecord.centralDirectorySize = raf.positionSync() - centralDirectoryOffset; + + final buffer = _endOfCentralDirectoryRecord.save(); + raf.writeListSync(buffer, 0, buffer.length); + + // TODO: Done saving, do something. + }); + }); + } /** * Open the Zip file for reading. - * - * Returns a future which gives a string containing an error message, if such ever occurred. */ Future open() { - var completer = new Completer(); - _initialized = true; + final completer = new Completer(); - this._file = new File.fromPath(this._filePath); - - this._file.readAsBytes().then((bytes) { - this._data = bytes; + _file = new File.fromPath(this._filePath); + _file.readAsBytes().then((bytes) { try { - this._process(); + final position = _getEndOfCentralDirectoryRecordPosition(bytes); + + _endOfCentralDirectoryRecord = new EndOfCentralDirectoryRecord.fromData(bytes.getRange(position, bytes.length - position)); + + // Create Central Directory object. + final centralDirectoryOffset = _endOfCentralDirectoryRecord.centralDirectoryOffset; + final centralDirectorySize = _endOfCentralDirectoryRecord.centralDirectorySize; + _centralDirectory = new CentralDirectory.fromData(bytes.getRange(centralDirectoryOffset, centralDirectorySize), bytes); + } on Exception catch (e) { completer.completeException(e); } @@ -60,24 +111,45 @@ class Zip { return completer.future; } + /** + * Adds the data associated with the given filename to the Zip. + */ + void addFileFromString(filename, String data) { + final fh = new LocalFileHeader(); + fh.content = data.charCodes(); + fh.crc32 = CRC32.compute(fh.content); + fh.uncompressedSize = data.length; + fh.compressedSize = fh.uncompressedSize; + fh.filename = filename; + + final cdfh = new CentralDirectoryFileHeader.fromLocalFileHeader(fh); + + _centralDirectory.fileHeaders.add(cdfh); + _endOfCentralDirectoryRecord.totalCentralDirectoryEntries += 1; + } + /** * Extracts the entire archive to the given path. */ - Future extractTo(Path path) { - var completer = new Completer(); + Future extractTo(path) { + if (path is String) + path = new Path(path); + + final completer = new Completer(); - // This method extracts every file, and we will call this later. + // This method extracts every file. We will call this later. void extract() { + // Create the target directory if needed. - var directory = new Directory.fromPath(path); + final directory = new Directory.fromPath(path); directory.create().then((directory) { // Extract every file. - this.files.forEach((CentralDirectoryFileHeader header) { - var filename = header.localFileHeader.filename; - var content = header.localFileHeader.content; + _centralDirectory.fileHeaders.forEach((CentralDirectoryFileHeader header) { + final filename = header.localFileHeader.filename; + final content = header.localFileHeader.content; - var file = new File.fromPath(path.append(filename)); + final file = new File.fromPath(path.append(filename)); // Open the file for writing. file.open(FileMode.WRITE).then((RandomAccessFile raf) { @@ -92,8 +164,8 @@ class Zip { } // If the Zip is not yet opened, open it first before we can extract it. - if (this._initialized == false) { - this.open().then((error) { + if (_file == null) { + open().then((error) { // Check for potential errors. if (error) { @@ -109,45 +181,25 @@ class Zip { return completer.future; } - /** - * Processes the Zip file contents. - */ - void _process() { - var position = this._getEndOfCentralDirectoryRecordPosition(); - if (position == false) { - throw new Exception('Could not locate the End of Central Directory Record. The archive seems to be a corrupted Zip archive.'); - } - - this._endOfCentralDirectoryRecord = new EndOfCentralDirectoryRecord(this._data.getRange(position, this._data.length - position)); - - // Create Central Directory object. - var centralDirectoryOffset = this._endOfCentralDirectoryRecord.centralDirectoryOffset; - var centralDirectorySize = this._endOfCentralDirectoryRecord.centralDirectorySize; - this._centralDirectory = new CentralDirectory(this._data.getRange(centralDirectoryOffset, centralDirectorySize), this._data); - - // Let the user access file headers. - this.files = this._centralDirectory.fileHeaders; - } - /** * Finds the position of the End of Central Directory. */ - int _getEndOfCentralDirectoryRecordPosition() { + int _getEndOfCentralDirectoryRecordPosition(bytes) { // I want to shoot the smart ass who had the great idea of having an arbitrary sized comment field in this header. - var signatureSize = Zip.END_OF_CENTRAL_DIRECTORY_RECORD_SIGNATURE.length; - var signatureCodes = Zip.END_OF_CENTRAL_DIRECTORY_RECORD_SIGNATURE.charCodes(); - var maxScanLength = 65536; - var length = this._data.length; + final signatureSize = Zip.END_OF_CENTRAL_DIRECTORY_RECORD_SIGNATURE.length; + final signatureCodes = Zip.END_OF_CENTRAL_DIRECTORY_RECORD_SIGNATURE.charCodes(); + final maxScanLength = 65536; + final length = bytes.length; var position = length - signatureSize; // Start looping from the end of the data sequence. for (; position > length - maxScanLength && position > 0; position--) { // If we found the end of central directory record signature, return the current position. - if (listsAreEqual(signatureCodes, this._data.getRange(position, signatureSize))) { + if (listsAreEqual(signatureCodes, bytes.getRange(position, signatureSize))) { return position; } } - return false; + throw new Exception('The Zip file seems to be corrupted. Could not find End of Central Directory Record location.'); } } \ No newline at end of file diff --git a/lib/central_directory.dart b/lib/central_directory.dart index f0d99b5..9090d23 100644 --- a/lib/central_directory.dart +++ b/lib/central_directory.dart @@ -16,23 +16,21 @@ import 'util.dart'; * Creates a new instance of the Central Directory. */ class CentralDirectory { - List _chunk; - List _data; + List content; static final FILE_HEADER_STATIC_SIZE = 46; // The static size of the file header. - List fileHeaders; + List fileHeaders = new List(); var digitalSignature; - CentralDirectory(List this._chunk, List this._data) { - this.fileHeaders = []; - this._process(); - } + CentralDirectory(); /** - * Reads the data and sets the information to class members. + * Instantiates a new Central Directory based on the chunk of data. + * + * The chunk will be parsed and appropriate Central Directory File Headers will be made. */ - void _process() { + CentralDirectory.fromData(List chunk, List this.content) { // [file header 1] // . // . @@ -71,23 +69,23 @@ class CentralDirectory { // Create file headers. Loop until we have gone through the entire buffer. while (true) { // Calculate sizes for dynamic parts. - final filenameSize = bytesToValue(this._chunk.getRange(28, 2)); - final extraFieldSize = bytesToValue(this._chunk.getRange(30, 2)); - final fileCommentSize = bytesToValue(this._chunk.getRange(32, 2)); + final filenameSize = bytesToValue(chunk.getRange(28, 2)); + final extraFieldSize = bytesToValue(chunk.getRange(30, 2)); + final fileCommentSize = bytesToValue(chunk.getRange(32, 2)); final dynamicSize = filenameSize + fileCommentSize + extraFieldSize; final totalFileHeaderSize = dynamicSize + FILE_HEADER_STATIC_SIZE; // Push a new file header. - if (this._chunk.length >= position + totalFileHeaderSize) { - final buffer = this._chunk.getRange(position, totalFileHeaderSize); - this.fileHeaders.add(new CentralDirectoryFileHeader(buffer, this._data)); + if (chunk.length >= position + totalFileHeaderSize) { + final buffer = chunk.getRange(position, totalFileHeaderSize); + this.fileHeaders.add(new CentralDirectoryFileHeader.fromData(buffer, this.content)); // Move the position pointer forward. position += totalFileHeaderSize; // Break out of the loop if the next 4 bytes do not match the right file header signature. - if (this._chunk.length >= position + signatureSize && !listsAreEqual(this._chunk.getRange(position, signatureSize), signatureCodes)) { + if (chunk.length >= position + signatureSize && !listsAreEqual(chunk.getRange(position, signatureSize), signatureCodes)) { break; } } else { diff --git a/lib/central_directory_file_header.dart b/lib/central_directory_file_header.dart index 6e08334..392f9de 100644 --- a/lib/central_directory_file_header.dart +++ b/lib/central_directory_file_header.dart @@ -17,16 +17,16 @@ import 'dart:utf'; * Creates a new instance of the Central Directory File Header. */ class CentralDirectoryFileHeader { - List _chunk; - List _data; + List content; LocalFileHeader localFileHeader; - var versionMadeBy; - var versionNeededToExtract; - var generalPurposeBitFlag; - var compressionMethod; - var lastModifiedFileTime; - var lastModifiedFileDate; - var crc32; + + var versionMadeBy = [0x00, 0x00]; + var versionNeededToExtract = [0x00, 0x00]; + var generalPurposeBitFlag = [0x00, 0x00]; + var compressionMethod = [0x00, 0x00]; + var lastModifiedFileTime = [0x00, 0x00]; + var lastModifiedFileDate = [0x00, 0x00]; + int crc32; var compressedSize; var uncompressedSize; var filenameLength; @@ -36,18 +36,60 @@ class CentralDirectoryFileHeader { var internalFileAttributes; var externalFileAttributes; var localHeaderOffset; - var filename; - var extraField; - var fileComment; + var filename = ''; + var extraField = []; + var fileComment = ''; + + CentralDirectoryFileHeader(); + + /** + * Creates a new Central Directory File Header based on the given Local File Header. + */ + CentralDirectoryFileHeader.fromLocalFileHeader(LocalFileHeader lfh) { + localFileHeader = lfh; + + filename = lfh.filename; + crc32 = lfh.crc32; + compressedSize = lfh.compressedSize; + uncompressedSize = lfh.uncompressedSize; + } + + /** + * Returns the bytes for this header. + */ + List save() { + var bytes = new List(); + + bytes.addAll(Zip.CENTRAL_DIRECTORY_FILE_HEADER_SIGNATURE.charCodes()); + bytes.addAll(versionMadeBy); + bytes.addAll(versionNeededToExtract); + bytes.addAll(generalPurposeBitFlag); + bytes.addAll(compressionMethod); + bytes.addAll(lastModifiedFileTime); + bytes.addAll(lastModifiedFileDate); + bytes.addAll(valueToBytes(crc32, 4)); + bytes.addAll(valueToBytes(compressedSize, 4)); + bytes.addAll(valueToBytes(uncompressedSize, 4)); + bytes.addAll(valueToBytes(filename.length, 2)); + bytes.addAll(valueToBytes(extraField.length, 2)); + bytes.addAll(valueToBytes(fileComment.length, 2)); + bytes.addAll(valueToBytes(diskNumberStart, 2)); + bytes.addAll(valueToBytes(internalFileAttributes, 2)); + bytes.addAll(valueToBytes(externalFileAttributes, 4)); + bytes.addAll(valueToBytes(localHeaderOffset, 4)); + bytes.addAll(filename.charCodes()); + bytes.addAll(extraField); + bytes.addAll(fileComment.charCodes()); - CentralDirectoryFileHeader(List this._chunk, List this._data) { - this._process(); + return bytes; } /** - * Reads the data and sets the information to class members. + * Instantiates a new Central Directory File Header based on the chunk of data. + * + * Every property will be set according to the bytes in the chunk. */ - void _process() { + CentralDirectoryFileHeader.fromData(List chunk, List this.content) { // Central directory file header: // // central file header signature 4 bytes (0x02014b50) @@ -72,27 +114,27 @@ class CentralDirectoryFileHeader { // extra field (variable size) // file comment (variable size) - this.versionMadeBy = this._chunk.getRange(4, 2); - this.versionNeededToExtract = this._chunk.getRange(6, 2); - this.generalPurposeBitFlag = this._chunk.getRange(8, 2); - this.compressionMethod = this._chunk.getRange(10, 2); - this.lastModifiedFileTime = this._chunk.getRange(12, 2); - this.lastModifiedFileDate = this._chunk.getRange(14, 2); - this.crc32 = this._chunk.getRange(16, 4); - this.compressedSize = bytesToValue(this._chunk.getRange(20, 4)); - this.uncompressedSize = bytesToValue(this._chunk.getRange(24, 4)); - this.filenameLength = bytesToValue(this._chunk.getRange(28, 2)); - this.extraFieldLength = bytesToValue(this._chunk.getRange(30, 2)); - this.fileCommentLength = bytesToValue(this._chunk.getRange(32, 2)); - this.diskNumberStart = bytesToValue(this._chunk.getRange(34, 2)); - this.internalFileAttributes = bytesToValue(this._chunk.getRange(36, 2)); - this.externalFileAttributes = bytesToValue(this._chunk.getRange(38, 4)); - this.localHeaderOffset = bytesToValue(this._chunk.getRange(42, 4)); - this.filename = new String.fromCharCodes(this._chunk.getRange(46, this.filenameLength)); - this.extraField = this._chunk.getRange(46 + this.filenameLength, this.extraFieldLength); - this.fileComment = this._chunk.getRange(46 + this.filenameLength + this.extraFieldLength, this.fileCommentLength); + versionMadeBy = chunk.getRange(4, 2); + versionNeededToExtract = chunk.getRange(6, 2); + generalPurposeBitFlag = chunk.getRange(8, 2); + compressionMethod = chunk.getRange(10, 2); + lastModifiedFileTime = chunk.getRange(12, 2); + lastModifiedFileDate = chunk.getRange(14, 2); + crc32 = bytesToValue(chunk.getRange(16, 4)); + compressedSize = bytesToValue(chunk.getRange(20, 4)); + uncompressedSize = bytesToValue(chunk.getRange(24, 4)); + filenameLength = bytesToValue(chunk.getRange(28, 2)); + extraFieldLength = bytesToValue(chunk.getRange(30, 2)); + fileCommentLength = bytesToValue(chunk.getRange(32, 2)); + diskNumberStart = bytesToValue(chunk.getRange(34, 2)); + internalFileAttributes = bytesToValue(chunk.getRange(36, 2)); + externalFileAttributes = bytesToValue(chunk.getRange(38, 4)); + localHeaderOffset = bytesToValue(chunk.getRange(42, 4)); + filename = new String.fromCharCodes(chunk.getRange(46, filenameLength)); + extraField = chunk.getRange(46 + filenameLength, extraFieldLength); + fileComment = new String.fromCharCodes(chunk.getRange(46 + filenameLength + extraFieldLength, fileCommentLength)); // TODO: Are there scenarios where LocalFileHeader.compressedSize != CentralDirectoryFileHeader.compressedSize? - this.localFileHeader = new LocalFileHeader(this._data.getRange(this.localHeaderOffset, this._data.length - this.localHeaderOffset)); + localFileHeader = new LocalFileHeader.fromData(content.getRange(localHeaderOffset, content.length - localHeaderOffset)); } } \ No newline at end of file diff --git a/lib/end_of_central_directory_record.dart b/lib/end_of_central_directory_record.dart index 19b5e5f..990c2bf 100644 --- a/lib/end_of_central_directory_record.dart +++ b/lib/end_of_central_directory_record.dart @@ -15,21 +15,42 @@ import 'util.dart'; * Creates a new instance of the End of Central Directory Record. */ class EndOfCentralDirectoryRecord { - List _chunk; - var totalCentralDirectoryEntries; + var numberOfThisDisk = [0x00, 0x00]; + var numberOfTheDiskWithTheStartOfTheCentralDirectory = [0x00, 0x00]; + var totalCentralDirectoryEntriesOnThisDisk = [0x00, 0x00]; + var totalCentralDirectoryEntries = 0; var centralDirectorySize; var centralDirectoryOffset; var zipFileCommentLength; - var zipFileComment; + var zipFileComment = ''; - EndOfCentralDirectoryRecord(List this._chunk) { - this._process(); + EndOfCentralDirectoryRecord(); + + /** + * Returns the bytes for this header. + */ + List save() { + var bytes = new List(); + + bytes.addAll(Zip.END_OF_CENTRAL_DIRECTORY_RECORD_SIGNATURE.charCodes()); + bytes.addAll(numberOfThisDisk); + bytes.addAll(numberOfTheDiskWithTheStartOfTheCentralDirectory); + bytes.addAll(totalCentralDirectoryEntriesOnThisDisk); + bytes.addAll(valueToBytes(totalCentralDirectoryEntries, 2)); + bytes.addAll(valueToBytes(centralDirectorySize, 4)); + bytes.addAll(valueToBytes(centralDirectoryOffset, 4)); + bytes.addAll(valueToBytes(zipFileComment.length, 2)); + bytes.addAll(zipFileComment.charCodes()); + + return bytes; } /** - * Reads the data and sets the information to class members. + * Instantiates a new End of Central Directory Record based on the chunk of data. + * + * Every property will be set according to the bytes in the chunk. */ - void _process() { + EndOfCentralDirectoryRecord.fromData(List chunk) { // I. End of central directory record: // // end of central dir signature 4 bytes (0x06054b50) @@ -47,10 +68,13 @@ class EndOfCentralDirectoryRecord { // .ZIP file comment length 2 bytes // .ZIP file comment (variable size) - this.totalCentralDirectoryEntries = bytesToValue(this._chunk.getRange(10, 2)); - this.centralDirectorySize = bytesToValue(this._chunk.getRange(12, 4)); - this.centralDirectoryOffset = bytesToValue(this._chunk.getRange(16, 4)); - this.zipFileCommentLength = bytesToValue(this._chunk.getRange(20, 2)); - this.zipFileComment = bytesToValue(this._chunk.getRange(22, this.zipFileCommentLength)); + numberOfThisDisk = bytesToValue(chunk.getRange(4, 2)); + numberOfTheDiskWithTheStartOfTheCentralDirectory = bytesToValue(chunk.getRange(6, 2)); + totalCentralDirectoryEntriesOnThisDisk = bytesToValue(chunk.getRange(8, 2)); + totalCentralDirectoryEntries = bytesToValue(chunk.getRange(10, 2)); + centralDirectorySize = bytesToValue(chunk.getRange(12, 4)); + centralDirectoryOffset = bytesToValue(chunk.getRange(16, 4)); + zipFileCommentLength = bytesToValue(chunk.getRange(20, 2)); + zipFileComment = new String.fromCharCodes(chunk.getRange(22, zipFileCommentLength)); } } \ No newline at end of file diff --git a/lib/local_file_header.dart b/lib/local_file_header.dart index e7a05cd..4542eed 100644 --- a/lib/local_file_header.dart +++ b/lib/local_file_header.dart @@ -13,27 +13,53 @@ import 'util.dart'; import 'dart:utf'; class LocalFileHeader { - List _chunk; List content; - var versionNeededToExtract; - var generalPurposeBitFlag; - var compressionMethod; - var lastModFileTime; - var lastModifiedFileTime; - var lastModifiedFileDate; - var crc32; + + var versionNeededToExtract = [0x00, 0x00]; + var generalPurposeBitFlag = [0x00, 0x00]; + var compressionMethod = [0x00, 0x00]; + var lastModifiedFileTime = [0x00, 0x00]; + var lastModifiedFileDate = [0x00, 0x00]; + int crc32; var compressedSize; var uncompressedSize; var filenameLength; var extraFieldLength; - var filename; - var extraField; + var filename = ''; + var extraField = []; + + LocalFileHeader(); + + /** + * Returns the bytes for this header. + */ + List save() { + var bytes = new List(); + + bytes.addAll(Zip.LOCAL_FILE_HEADER_SIGNATURE.charCodes()); + bytes.addAll(versionNeededToExtract); + bytes.addAll(generalPurposeBitFlag); + bytes.addAll(compressionMethod); + bytes.addAll(lastModifiedFileTime); + bytes.addAll(lastModifiedFileDate); + bytes.addAll(valueToBytes(crc32, 4)); + bytes.addAll(valueToBytes(compressedSize, 4)); + bytes.addAll(valueToBytes(uncompressedSize, 4)); + bytes.addAll(valueToBytes(filename.length, 2)); + bytes.addAll(valueToBytes(extraField.length, 2)); + bytes.addAll(filename.charCodes()); + bytes.addAll(extraField); + bytes.addAll(content); - LocalFileHeader(List this._chunk) { - this._process(); + return bytes; } - void _process() { + /** + * Instantiates a new Local File Header based on the chunk of data. + * + * Every property will be set according to the bytes in the chunk. + */ + LocalFileHeader.fromData(List chunk) { // Local file header: // // local file header signature 4 bytes (0x04034b50) @@ -51,24 +77,24 @@ class LocalFileHeader { // file name (variable size) // extra field (variable size) - assert(this._chunk.getRange(0, 4) == Zip.LOCAL_FILE_HEADER_SIGNATURE); + assert(chunk.getRange(0, 4) == Zip.LOCAL_FILE_HEADER_SIGNATURE); - this.versionNeededToExtract = this._chunk.getRange(4, 2); - this.generalPurposeBitFlag = this._chunk.getRange(6, 2); - this.compressionMethod = this._chunk.getRange(8, 2); - this.lastModifiedFileTime = this._chunk.getRange(10, 2); - this.lastModifiedFileDate = this._chunk.getRange(12, 2); - this.crc32 = this._chunk.getRange(14, 4); - this.compressedSize = bytesToValue(this._chunk.getRange(18, 4)); - this.uncompressedSize = bytesToValue(this._chunk.getRange(22, 4)); - this.filenameLength = bytesToValue(this._chunk.getRange(26, 2)); - this.extraFieldLength = bytesToValue(this._chunk.getRange(28, 2)); - this.filename = new String.fromCharCodes(this._chunk.getRange(30, this.filenameLength)); + versionNeededToExtract = chunk.getRange(4, 2); + generalPurposeBitFlag = chunk.getRange(6, 2); + compressionMethod = chunk.getRange(8, 2); + lastModifiedFileTime = chunk.getRange(10, 2); + lastModifiedFileDate = chunk.getRange(12, 2); + crc32 = bytesToValue(chunk.getRange(14, 4)); + compressedSize = bytesToValue(chunk.getRange(18, 4)); + uncompressedSize = bytesToValue(chunk.getRange(22, 4)); + filenameLength = bytesToValue(chunk.getRange(26, 2)); + extraFieldLength = bytesToValue(chunk.getRange(28, 2)); + filename = new String.fromCharCodes(chunk.getRange(30, filenameLength)); - if (this.extraFieldLength) { - this.extraField = this._chunk.getRange(30 + this.filenameLength, this.extraFieldLength); + if (extraFieldLength) { + extraField = chunk.getRange(30 + filenameLength, extraFieldLength); } - this.content = this._chunk.getRange(30 + this.filenameLength + this.extraFieldLength, this.compressedSize); + content = chunk.getRange(30 + filenameLength + extraFieldLength, compressedSize); } } \ No newline at end of file diff --git a/lib/util.dart b/lib/util.dart new file mode 100644 index 0000000..ab17b6b --- /dev/null +++ b/lib/util.dart @@ -0,0 +1,52 @@ +library Util; + +import 'dart:math'; + +/** + * Converts the byte sequence to a numeric representation. + */ +int bytesToValue(List bytes) { + var value = 0; + + for (var i = 0, length = bytes.length; i < length; i++) { + value += bytes[i] * pow(256, i); + } + + return value; +} + +/** + * Converts the given value to a byte sequence. + * + * The parameter [minByteCount] specifies how many bytes should be returned at least. + */ +List valueToBytes(int value, [int minByteCount = 0]) { + var bytes = [0x00, 0x00, 0x00, 0x00]; + + if (value == null) value = 0; + + var i = 0; + var actualByteCount = 0; + do { + bytes[i++] = value & (255); + value = value >> 8; + actualByteCount += 1; + + if (value == 0) + break; + } while (i < 4); + + return bytes.getRange(0, max(minByteCount, actualByteCount)); +} + +/** + * Returns true if the two given lists are equal. + */ +bool listsAreEqual(final List one, final List two) { + var i = -1; + return one.every((element) { + i++; + + return two[i] == element; + }); +} \ No newline at end of file diff --git a/lib/zip.dart b/lib/zip.dart new file mode 100644 index 0000000..e176aa2 --- /dev/null +++ b/lib/zip.dart @@ -0,0 +1,205 @@ +/*! + * DartZip + * + * Copyright (C) 2012, Kai Sellgren + * Licensed under the MIT License. + * http://www.opensource.org/licenses/mit-license.php + */ + +library zip; + +import 'dart:io'; +import 'end_of_central_directory_record.dart'; +import 'central_directory.dart'; +import 'central_directory_file_header.dart'; +import 'local_file_header.dart'; +import 'util.dart'; +import 'package:crc32/crc32.dart'; + +/** + * This class represents a Zip file. + */ +class Zip { + static final LOCAL_FILE_HEADER_SIGNATURE = "\x50\x4b\x03\x04"; + static final DATA_DESCRIPTOR_SIGNATURE = "\x50\x4b\x07\x08"; + static final CENTRAL_DIRECTORY_FILE_HEADER_SIGNATURE = "\x50\x4b\x01\x02"; + static final END_OF_CENTRAL_DIRECTORY_RECORD_SIGNATURE = "\x50\x4b\x05\x06"; + static final CENTRAL_DIRECTORY_DIGITAL_SIGNATURE_SIGNATURE = "\x50\x4b\x05\x05"; + + Path _filePath; + File _file; + + EndOfCentralDirectoryRecord _endOfCentralDirectoryRecord = new EndOfCentralDirectoryRecord(); + CentralDirectory _centralDirectory = new CentralDirectory(); + + Zip(path) { + if (path is String) + _filePath = new Path(path); + else + _filePath = path; + } + + /** + * Saves the Zip archive. + */ + void save() { + _file = new File.fromPath(this._filePath); + + _file.create().then((File file) { + file.open(FileMode.WRITE).then((RandomAccessFile raf) { + + // Start by writing Local File Headers. + _centralDirectory.fileHeaders.forEach((CentralDirectoryFileHeader cdfh) { + + // Save the current position on the Central Directory File Header. + cdfh.localHeaderOffset = raf.positionSync(); + + // Save the header and write it to the file. + final buffer = cdfh.localFileHeader.save(); + raf.writeListSync(buffer, 0, buffer.length); + }); + + // We are now at the location of the Central Directory. + final centralDirectoryOffset = raf.positionSync(); + + // Continue with the Central Directory Record. + _centralDirectory.fileHeaders.forEach((CentralDirectoryFileHeader cdfh) { + + // Save the header and write it to the file. + final buffer = cdfh.save(); + raf.writeListSync(buffer, 0, buffer.length); + }); + + // Last, write the End of Central Directory Record. + _endOfCentralDirectoryRecord.centralDirectoryOffset = centralDirectoryOffset; + _endOfCentralDirectoryRecord.centralDirectorySize = raf.positionSync() - centralDirectoryOffset; + + final buffer = _endOfCentralDirectoryRecord.save(); + raf.writeListSync(buffer, 0, buffer.length); + + // TODO: Done saving, do something. + }); + }); + } + + /** + * Open the Zip file for reading. + */ + Future open() { + final completer = new Completer(); + + _file = new File.fromPath(this._filePath); + + _file.readAsBytes().then((bytes) { + try { + final position = _getEndOfCentralDirectoryRecordPosition(bytes); + + _endOfCentralDirectoryRecord = new EndOfCentralDirectoryRecord.fromData(bytes.getRange(position, bytes.length - position)); + + // Create Central Directory object. + final centralDirectoryOffset = _endOfCentralDirectoryRecord.centralDirectoryOffset; + final centralDirectorySize = _endOfCentralDirectoryRecord.centralDirectorySize; + _centralDirectory = new CentralDirectory.fromData(bytes.getRange(centralDirectoryOffset, centralDirectorySize), bytes); + + } on Exception catch (e) { + completer.completeException(e); + } + + completer.complete(null); + }); + + return completer.future; + } + + /** + * Adds the data associated with the given filename to the Zip. + */ + void addFileFromString(filename, String data) { + final fh = new LocalFileHeader(); + fh.content = data.charCodes(); + fh.crc32 = CRC32.compute(fh.content); + fh.uncompressedSize = data.length; + fh.compressedSize = fh.uncompressedSize; + fh.filename = filename; + + final cdfh = new CentralDirectoryFileHeader.fromLocalFileHeader(fh); + + _centralDirectory.fileHeaders.add(cdfh); + _endOfCentralDirectoryRecord.totalCentralDirectoryEntries += 1; + } + + /** + * Extracts the entire archive to the given path. + */ + Future extractTo(path) { + if (path is String) + path = new Path(path); + + final completer = new Completer(); + + // This method extracts every file. We will call this later. + void extract() { + + // Create the target directory if needed. + final directory = new Directory.fromPath(path); + directory.create().then((directory) { + + // Extract every file. + _centralDirectory.fileHeaders.forEach((CentralDirectoryFileHeader header) { + final filename = header.localFileHeader.filename; + final content = header.localFileHeader.content; + + final file = new File.fromPath(path.append(filename)); + + // Open the file for writing. + file.open(FileMode.WRITE).then((RandomAccessFile raf) { + + // Write all bytes and then close the file. + raf.writeList(content, 0, content.length).then((trash) => raf.close()); + }); + }); + + completer.complete(null); + }); + } + + // If the Zip is not yet opened, open it first before we can extract it. + if (_file == null) { + open().then((error) { + + // Check for potential errors. + if (error) { + completer.completeException(error); + } else { + extract(); + } + }); + } else { + extract(); + } + + return completer.future; + } + + /** + * Finds the position of the End of Central Directory. + */ + int _getEndOfCentralDirectoryRecordPosition(bytes) { + // I want to shoot the smart ass who had the great idea of having an arbitrary sized comment field in this header. + final signatureSize = Zip.END_OF_CENTRAL_DIRECTORY_RECORD_SIGNATURE.length; + final signatureCodes = Zip.END_OF_CENTRAL_DIRECTORY_RECORD_SIGNATURE.charCodes(); + final maxScanLength = 65536; + final length = bytes.length; + var position = length - signatureSize; + + // Start looping from the end of the data sequence. + for (; position > length - maxScanLength && position > 0; position--) { + // If we found the end of central directory record signature, return the current position. + if (listsAreEqual(signatureCodes, bytes.getRange(position, signatureSize))) { + return position; + } + } + + throw new Exception('The Zip file seems to be corrupted. Could not find End of Central Directory Record location.'); + } +} \ No newline at end of file diff --git a/test/test.dart b/test/test.dart index 594ee6a..d3a6589 100644 --- a/test/test.dart +++ b/test/test.dart @@ -2,11 +2,11 @@ import '../lib/zip.dart'; import 'dart:io'; void main() { - final zip = new Zip(new Path('test.zip')); - //zip.addFileFromString('something.txt', 'content goes here'); - //zip.save(); + final zip = new Zip('test.zip'); + zip.addFileFromString('something.txt', 'content goes here'); + zip.save(); - zip.extractTo(new Path.fromNative("${new Directory.current().path}/test-extraction/")); + //zip.extractTo(new Path.fromNative("${new Directory.current().path}/test-extraction/")); /* zip.open().then((error) {