Skip to content

Commit

Permalink
Major improvements to creating archives.
Browse files Browse the repository at this point in the history
  • Loading branch information
kaisellgren committed Sep 26, 2012
1 parent 70ccb2e commit 5325bfb
Show file tree
Hide file tree
Showing 9 changed files with 574 additions and 151 deletions.
28 changes: 26 additions & 2 deletions lib/Util.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import 'dart:math';
/**
* Converts the byte sequence to a numeric representation.
*/
bytesToValue(List<int> bytes) {
int bytesToValue(List<int> bytes) {
var value = 0;

for (var i = 0, length = bytes.length; i < length; i++) {
Expand All @@ -15,10 +15,34 @@ bytesToValue(List<int> bytes) {
return value;
}

/**
* Converts the given value to a byte sequence.
*
* The parameter [minByteCount] specifies how many bytes should be returned at least.
*/
List<int> 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++;
Expand Down
158 changes: 105 additions & 53 deletions lib/Zip.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -25,31 +28,79 @@ class Zip {

Path _filePath;
File _file;
List<int> _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<Exception> 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);
}
Expand All @@ -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<Exception> extractTo(Path path) {
var completer = new Completer();
Future<Exception> 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) {
Expand All @@ -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) {
Expand All @@ -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.');
}
}
30 changes: 14 additions & 16 deletions lib/central_directory.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,21 @@ import 'util.dart';
* Creates a new instance of the Central Directory.
*/
class CentralDirectory {
List<int> _chunk;
List<int> _data;
List<int> content;

static final FILE_HEADER_STATIC_SIZE = 46; // The static size of the file header.

List<CentralDirectoryFileHeader> fileHeaders;
List<CentralDirectoryFileHeader> fileHeaders = new List();
var digitalSignature;

CentralDirectory(List<int> this._chunk, List<int> 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<int> chunk, List<int> this.content) {
// [file header 1]
// .
// .
Expand Down Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit 5325bfb

Please sign in to comment.