Add components for zip packing.
This is the changes to add packing feature in zip unpacker.
Design doc (now writing) can be found in the following link.
https://docs.google.com/a/google.com/document/d/1eQQEkkwdEPfVANnlaJxIP12MZjaAcJ22ziIiuj0DMIw/edit?usp=sharing
BUG=359837
TEST=manually tested(test components will be added later.)
Change-Id: Ia0ad78050a58053aa09e268f25ac6f064cbb9aac
diff --git a/unpacker/Makefile.pnacl b/unpacker/Makefile.pnacl
index cacf5e8..eb2766c 100644
--- a/unpacker/Makefile.pnacl
+++ b/unpacker/Makefile.pnacl
@@ -19,6 +19,9 @@
CFLAGS = -Wall
SOURCES = \
+ cpp/compressor.cc \
+ cpp/compressor_archive_libarchive.cc \
+ cpp/compressor_io_javascript_stream.cc \
cpp/module.cc \
cpp/request.cc \
cpp/volume.cc \
diff --git a/unpacker/cpp/compressor.cc b/unpacker/cpp/compressor.cc
new file mode 100644
index 0000000..1d13d78
--- /dev/null
+++ b/unpacker/cpp/compressor.cc
@@ -0,0 +1,140 @@
+// Copyright 2017 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "compressor.h"
+
+#include <cstring>
+#include <ctime>
+#include <sstream>
+
+#include "request.h"
+#include "compressor_io_javascript_stream.h"
+#include "compressor_archive_libarchive.h"
+
+namespace {
+
+// An internal implementation of JavaScriptCompressorRequestorInterface.
+class JavaScriptCompressorRequestor : public JavaScriptCompressorRequestorInterface {
+ public:
+ explicit JavaScriptCompressorRequestor(Compressor* compressor) :
+ compressor_(compressor) {}
+
+ virtual void WriteChunkRequest(int64_t length,
+ const pp::VarArrayBuffer& buffer) {
+ compressor_->message_sender()->SendWriteChunk(
+ compressor_->compressor_id(), buffer, length);
+ }
+
+ virtual void ReadFileChunkRequest(int64_t length) {
+ compressor_->message_sender()->SendReadFileChunk(
+ compressor_->compressor_id(), length);
+ }
+
+ private:
+ Compressor* compressor_;
+};
+
+} // namespace
+
+Compressor::Compressor(const pp::InstanceHandle& instance_handle,
+ int compressor_id,
+ JavaScriptMessageSenderInterface* message_sender)
+ : compressor_id_(compressor_id),
+ message_sender_(message_sender),
+ worker_(instance_handle),
+ callback_factory_(this) {
+ requestor_ = new JavaScriptCompressorRequestor(this);
+ compressor_stream_ =
+ new CompressorIOJavaScriptStream(requestor_);
+ compressor_archive_ =
+ new CompressorArchiveLibarchive(compressor_stream_);
+}
+
+Compressor::~Compressor() {
+ worker_.Join();
+ delete compressor_archive_;
+ delete compressor_stream_;
+ delete requestor_;
+}
+
+bool Compressor::Init() {
+ return worker_.Start();
+}
+
+void Compressor::CreateArchive() {
+ compressor_archive_->CreateArchive();
+ message_sender_->SendCreateArchiveDone(compressor_id_);
+}
+
+void Compressor::AddToArchive(const pp::VarDictionary& dictionary) {
+ worker_.message_loop().PostWork(callback_factory_.NewCallback(
+ &Compressor::AddToArchiveCallback, dictionary));
+}
+
+void Compressor::AddToArchiveCallback(int32_t,
+ const pp::VarDictionary& dictionary) {
+ PP_DCHECK(dictionary.Get(request::key::kPathname).is_string());
+ std::string pathname =
+ dictionary.Get(request::key::kPathname).AsString();
+
+ PP_DCHECK(dictionary.Get(request::key::kFileSize).is_string());
+ int64_t file_size =
+ request::GetInt64FromString(dictionary, request::key::kFileSize);
+ PP_DCHECK(file_size >= 0);
+
+ PP_DCHECK(dictionary.Get(request::key::kIsDirectory).is_bool());
+ bool is_directory =
+ dictionary.Get(request::key::kIsDirectory).AsBool();
+
+ PP_DCHECK(dictionary.Get(request::key::kModificationTime).is_string());
+ std::string strtime =
+ dictionary.Get(request::key::kModificationTime).AsString();
+ tm tm;
+ strptime(strtime.c_str(), "%m/%d/%Y %T", &tm);
+ time_t modification_time = mktime(&tm);
+
+ compressor_archive_->AddToArchive(
+ pathname, file_size, modification_time, is_directory);
+ message_sender_->SendAddToArchiveDone(compressor_id_);
+}
+
+void Compressor::ReadFileChunkDone(const pp::VarDictionary& dictionary) {
+ PP_DCHECK(dictionary.Get(request::key::kLength).is_string());
+ int64_t read_bytes =
+ request::GetInt64FromString(dictionary, request::key::kLength);
+
+ PP_DCHECK(dictionary.Get(request::key::kChunkBuffer).is_array_buffer());
+ pp::VarArrayBuffer array_buffer(dictionary.Get(request::key::kChunkBuffer));
+
+ compressor_stream_->ReadFileChunkDone(read_bytes, &array_buffer);
+}
+
+void Compressor::WriteChunkDone(const pp::VarDictionary& dictionary) {
+ PP_DCHECK(dictionary.Get(request::key::kLength).is_string());
+ int64_t written_bytes =
+ request::GetInt64FromString(dictionary, request::key::kLength);
+
+ compressor_stream_->WriteChunkDone(written_bytes);
+}
+
+void Compressor::CloseArchive(const pp::VarDictionary& dictionary) {
+ PP_DCHECK(dictionary.Get(request::key::kHasError).is_bool());
+ bool has_error =
+ dictionary.Get(request::key::kHasError).AsBool();
+
+ // If an error has occurred, no more write chunk requests are sent and
+ // CloseArchive() can be safely called in the main thread.
+ if (has_error) {
+ compressor_archive_->CloseArchive(has_error);
+ message_sender_->SendCloseArchiveDone(compressor_id_);
+ } else {
+ worker_.message_loop().PostWork(callback_factory_.NewCallback(
+ &Compressor::CloseArchiveCallback, has_error));
+ }
+}
+
+void Compressor::CloseArchiveCallback(int32_t, bool has_error) {
+ compressor_archive_->CloseArchive(has_error);
+ message_sender_->SendCloseArchiveDone(compressor_id_);
+}
diff --git a/unpacker/cpp/compressor.h b/unpacker/cpp/compressor.h
new file mode 100644
index 0000000..99236a1
--- /dev/null
+++ b/unpacker/cpp/compressor.h
@@ -0,0 +1,93 @@
+// Copyright 2017 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPRESSOR_H_
+#define COMPRESSOR_H_
+
+#include <ctime>
+#include <pthread.h>
+
+#include "archive.h"
+#include "ppapi/cpp/instance_handle.h"
+#include "ppapi/cpp/var_array_buffer.h"
+#include "ppapi/cpp/var_dictionary.h"
+#include "ppapi/utility/completion_callback_factory.h"
+#include "ppapi/utility/threading/simple_thread.h"
+
+#include "compressor_archive.h"
+#include "compressor_stream.h"
+#include "javascript_compressor_requestor_interface.h"
+#include "javascript_message_sender_interface.h"
+
+// Handles all packing operations like creating archive objects and writing data
+// onto the archive.
+class Compressor {
+ public:
+ Compressor(const pp::InstanceHandle& instance_handle /* Used for workers. */,
+ int compressor_id,
+ JavaScriptMessageSenderInterface* message_sender);
+
+ virtual ~Compressor();
+
+ // Initializes the compressor.
+ bool Init();
+
+ // Creates an archive object.
+ void CreateArchive();
+
+ // Adds an entry to the archive.
+ void AddToArchive(const pp::VarDictionary& dictionary);
+
+ // Processes a file chunk sent from JavaScript.
+ void ReadFileChunkDone(const pp::VarDictionary& dictionary);
+
+ // Receives a write chunk response from JavaScript.
+ void WriteChunkDone(const pp::VarDictionary& dictionary);
+
+ // Releases all resources obtained by libarchive.
+ void CloseArchive(const pp::VarDictionary& dictionary);
+
+ // A getter function for the message sender.
+ JavaScriptMessageSenderInterface* message_sender() { return message_sender_; }
+
+ // A getter function for the requestor.
+ JavaScriptCompressorRequestorInterface* requestor() { return requestor_; }
+
+ // A getter function for the compressor id.
+ int compressor_id() { return compressor_id_; }
+
+ private:
+
+ // A callback helper for AddToArchive.
+ void AddToArchiveCallback(int32_t, const pp::VarDictionary& dictionary);
+
+ // A callback helper for CloseArchive.
+ void CloseArchiveCallback(int32_t, bool has_error);
+
+ // The compressor id of this compressor.
+ int compressor_id_;
+
+ // An object that sends messages to JavaScript.
+ JavaScriptMessageSenderInterface* message_sender_;
+
+ // A worker for jobs that require blocking operations or a lot of processing
+ // time. Those shouldn't be done on the main thread. The jobs submitted to
+ // this thread are executed in order, so a new job must wait for the last job
+ // to finish.
+ pp::SimpleThread worker_;
+
+ // Callback factory used to submit jobs to worker_.
+ pp::CompletionCallbackFactory<Compressor> callback_factory_;
+
+ // A requestor for making calls to JavaScript.
+ JavaScriptCompressorRequestorInterface* requestor_;
+
+ // Libarchive wrapper instance per compressor, shared across all operations.
+ CompressorArchive* compressor_archive_;
+
+ // An instance that takes care of all IO operations.
+ CompressorStream* compressor_stream_;
+};
+
+#endif /// COMPRESSOR_H_
diff --git a/unpacker/cpp/compressor_archive.h b/unpacker/cpp/compressor_archive.h
new file mode 100644
index 0000000..3c9ff88
--- /dev/null
+++ b/unpacker/cpp/compressor_archive.h
@@ -0,0 +1,54 @@
+// Copyright 2017 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPRESSSOR_ARCHIVE_H_
+#define COMPRESSSOR_ARCHIVE_H_
+
+#include "compressor_io_javascript_stream.h"
+
+// Defines a wrapper for packing operations executed on an archive. API is not
+// meant to be thread safe and its methods shouldn't be called in parallel.
+class CompressorArchive {
+ public:
+ explicit CompressorArchive(CompressorStream* compressor_stream)
+ : compressor_stream_(compressor_stream) {}
+
+ virtual ~CompressorArchive() {}
+
+ // Creates an archive object. This method does not call CustomArchiveWrite, so
+ // this is synchronous.
+ virtual void CreateArchive() = 0;
+
+ // Releases all resources obtained by libarchive.
+ // This method also writes metadata about the archive itself onto the end of
+ // the archive file before releasing resources if hasError is false. Since
+ // writing data onto the archive is asynchronous, this function must not be
+ // called in the main thread if hasError is false.
+ virtual void CloseArchive(bool has_error) = 0;
+
+ // Adds an entry to the archive. It writes the header of the entry onto the
+ // archive first, and then if it is a file(not a directory), requests
+ // JavaScript for file chunks, compresses and writes them onto the archive
+ // until all chunks of the entry are written onto the archive. This method
+ // calls IO operations, so this function must not be called in the main thread.
+ virtual void AddToArchive(const std::string& filename,
+ int64_t file_size,
+ time_t modification_time,
+ bool is_directory) = 0;
+
+ // A getter function for archive_.
+ struct archive* archive() const { return archive_; }
+
+ // A getter function for compressor_stream_.
+ CompressorStream* compressor_stream() const { return compressor_stream_; }
+
+ private:
+ // The libarchive correspondent archive object.
+ struct archive* archive_;
+
+ // An instance that takes care of all IO operations.
+ CompressorStream* compressor_stream_;
+};
+
+#endif // COMPRESSSOR_ARCHIVE_H_
diff --git a/unpacker/cpp/compressor_archive_libarchive.cc b/unpacker/cpp/compressor_archive_libarchive.cc
new file mode 100644
index 0000000..92ac415
--- /dev/null
+++ b/unpacker/cpp/compressor_archive_libarchive.cc
@@ -0,0 +1,147 @@
+// Copyright 2017 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "compressor_archive_libarchive.h"
+
+#include <cerrno>
+#include <cstring>
+
+#include "archive_entry.h"
+#include "ppapi/cpp/logging.h"
+
+namespace {
+ // Nothing to do here because JavaScript takes care of file open operations.
+ int CustomArchiveOpen(archive* archive_object, void* client_data) {
+ return ARCHIVE_OK;
+ }
+
+ // Called when any data chunk must be written on the archive. It copies data
+ // from the given buffer processed by libarchive to an array buffer and passes
+ // it to compressor_stream.
+ ssize_t CustomArchiveWrite(archive* archive_object, void* client_data,
+ const void* buffer, size_t length) {
+ CompressorArchiveLibarchive* compressor_libarchive =
+ static_cast<CompressorArchiveLibarchive*>(client_data);
+
+ const char* char_buffer = static_cast<const char*>(buffer);
+
+ // Copy the data in buffer to array_buffer.
+ PP_DCHECK(length > 0);
+ pp::VarArrayBuffer array_buffer(length);
+ char* array_buffer_data = static_cast<char*>(array_buffer.Map());
+ memcpy(array_buffer_data, char_buffer, length);
+ array_buffer.Unmap();
+
+ ssize_t written_bytes =
+ compressor_libarchive->compressor_stream()->Write(length, array_buffer);
+
+ // Negative written_bytes represents an error.
+ if (written_bytes < 0) {
+ // When writing fails, archive_set_error() should be called and -1 should
+ // be returned.
+ archive_set_error(
+ compressor_libarchive->archive(), EIO, "Failed to write a chunk.");
+ return -1;
+ }
+ return written_bytes;
+ }
+
+ // Nothing to do here because JavaScript takes care of file close operations.
+ int CustomArchiveClose(archive* archive_object, void* client_data) {
+ return ARCHIVE_OK;
+ }
+}
+
+CompressorArchiveLibarchive::CompressorArchiveLibarchive(
+ CompressorStream* compressor_stream)
+ : CompressorArchive(compressor_stream),
+ compressor_stream_(compressor_stream) {
+ destination_buffer_ =
+ new char[compressor_archive_constants::kMaximumDataChunkSize];
+}
+
+CompressorArchiveLibarchive::~CompressorArchiveLibarchive() {
+ delete destination_buffer_;
+}
+
+void CompressorArchiveLibarchive::CreateArchive() {
+ archive_ = archive_write_new();
+ archive_write_set_format_zip(archive_);
+
+ // Passing 1 as the second argument causes the final chunk not to be padded.
+ archive_write_set_bytes_in_last_block(archive_, 1);
+ archive_write_set_bytes_per_block(
+ archive_, compressor_archive_constants::kMaximumDataChunkSize);
+ archive_write_open(archive_, this, CustomArchiveOpen,
+ CustomArchiveWrite, CustomArchiveClose);
+}
+
+void CompressorArchiveLibarchive::AddToArchive(
+ const std::string& filename,
+ int64_t file_size,
+ time_t modification_time,
+ bool is_directory) {
+ entry = archive_entry_new();
+
+ archive_entry_set_pathname(entry, filename.c_str());
+ archive_entry_set_size(entry, file_size);
+ archive_entry_set_mtime(entry, modification_time, 0 /* millisecond */);
+
+ if (is_directory) {
+ archive_entry_set_filetype(entry, AE_IFDIR);
+ archive_entry_set_perm(
+ entry, compressor_archive_constants::kDirectoryPermission);
+ } else {
+ archive_entry_set_filetype(entry, AE_IFREG);
+ archive_entry_set_perm(
+ entry, compressor_archive_constants::kFilePermission);
+ }
+ archive_write_header(archive_, entry);
+ // If archive_errno() returns 0, the header was written correctly.
+ if (archive_errno(archive_) != 0) {
+ CloseArchive(true /* hasError */);
+ return;
+ }
+
+ if (!is_directory) {
+ int64_t remaining_size = file_size;
+ while (remaining_size > 0) {
+ int64_t chunk_size = std::min(remaining_size,
+ compressor_archive_constants::kMaximumDataChunkSize);
+ PP_DCHECK(chunk_size > 0);
+
+ int64_t read_bytes =
+ compressor_stream_->Read(chunk_size, destination_buffer_);
+ // Negative read_bytes indicates an error occurred when reading chunks.
+ if (read_bytes < 0) {
+ CloseArchive(true /* hasError */);
+ break;
+ }
+
+ int64_t written_bytes =
+ archive_write_data(archive_, destination_buffer_, read_bytes);
+ // If archive_errno() returns 0, the buffer was written correctly.
+ if (archive_errno(archive_) != 0) {
+ CloseArchive(true /* hasError */);
+ break;
+ }
+ PP_DCHECK(written_bytes > 0);
+
+ remaining_size -= written_bytes;
+ }
+ }
+
+ archive_entry_free(entry);
+}
+
+void CompressorArchiveLibarchive::CloseArchive(bool has_error) {
+ // If has_error is true, mark the archive object as being unusable and
+ // release resources without writing no more data on the archive.
+ if (has_error)
+ archive_write_fail(archive_);
+ if (archive_) {
+ archive_write_free(archive_);
+ archive_ = NULL;
+ }
+}
diff --git a/unpacker/cpp/compressor_archive_libarchive.h b/unpacker/cpp/compressor_archive_libarchive.h
new file mode 100644
index 0000000..b0a299d
--- /dev/null
+++ b/unpacker/cpp/compressor_archive_libarchive.h
@@ -0,0 +1,61 @@
+// Copyright 2017 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPRESSOR_ARCHIVE_LIBARCHIVE_H_
+#define COMPRESSOR_ARCHIVE_LIBARCHIVE_H_
+
+#include <string>
+
+#include "archive.h"
+
+#include "compressor_archive.h"
+#include "compressor_stream.h"
+
+// A namespace with constants used by CompressorArchiveLibarchive.
+namespace compressor_archive_constants {
+const int64_t kMaximumDataChunkSize = 512 * 1024;
+const int kFilePermission = 640;
+const int kDirectoryPermission = 760;
+} // namespace compressor_archive_constants
+
+class CompressorArchiveLibarchive : public CompressorArchive {
+ public:
+ explicit CompressorArchiveLibarchive(CompressorStream* compressor_stream);
+
+ virtual ~CompressorArchiveLibarchive();
+
+ // Creates an archive object.
+ virtual void CreateArchive();
+
+ // Releases all resources obtained by libarchive.
+ virtual void CloseArchive(bool has_error);
+
+ // Adds an entry to the archive.
+ virtual void AddToArchive(const std::string& filename,
+ int64_t file_size,
+ time_t modification_time,
+ bool is_directory);
+
+ // A getter function for archive_.
+ struct archive* archive() const { return archive_; }
+
+ // A getter function for compressor_stream.
+ CompressorStream* compressor_stream() const { return compressor_stream_; }
+
+ private:
+ // An instance that takes care of all IO operations.
+ CompressorStream* compressor_stream_;
+
+ // The libarchive correspondent archive object.
+ struct archive* archive_;
+
+ // The libarchive correspondent archive entry object that is currently
+ // processed.
+ struct archive_entry* entry;
+
+ // The buffer used to store the data read from JavaScript.
+ char* destination_buffer_;
+};
+
+#endif // COMPRESSOR_ARCHIVE_LIBARCHIVE_H_
diff --git a/unpacker/cpp/compressor_io_javascript_stream.cc b/unpacker/cpp/compressor_io_javascript_stream.cc
new file mode 100644
index 0000000..5fdb081
--- /dev/null
+++ b/unpacker/cpp/compressor_io_javascript_stream.cc
@@ -0,0 +1,84 @@
+// Copyright 2017 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "compressor_io_javascript_stream.h"
+
+#include <limits>
+#include <thread>
+
+#include "archive.h"
+#include "ppapi/cpp/logging.h"
+
+CompressorIOJavaScriptStream::CompressorIOJavaScriptStream(
+ JavaScriptCompressorRequestorInterface* requestor)
+ : requestor_(requestor) {
+ pthread_mutex_init(&shared_state_lock_, NULL);
+ pthread_cond_init(&available_data_cond_, NULL);
+ pthread_cond_init(&data_written_cond_, NULL);
+
+ pthread_mutex_lock(&shared_state_lock_);
+ available_data_ = false;
+ pthread_mutex_unlock(&shared_state_lock_);
+}
+
+CompressorIOJavaScriptStream::~CompressorIOJavaScriptStream() {
+ pthread_cond_destroy(&data_written_cond_);
+ pthread_cond_destroy(&available_data_cond_);
+ pthread_mutex_destroy(&shared_state_lock_);
+};
+
+int64_t CompressorIOJavaScriptStream::Write(int64_t byte_to_write,
+ const pp::VarArrayBuffer& buffer) {
+ pthread_mutex_lock(&shared_state_lock_);
+ requestor_->WriteChunkRequest(byte_to_write, buffer);
+
+ pthread_cond_wait(&data_written_cond_, &shared_state_lock_);
+
+ int64_t written_bytes = written_bytes_;
+ pthread_mutex_unlock(&shared_state_lock_);
+
+ return written_bytes;
+}
+
+void CompressorIOJavaScriptStream::WriteChunkDone(int64_t written_bytes) {
+ pthread_mutex_lock(&shared_state_lock_);
+ written_bytes_ = written_bytes;
+ pthread_cond_signal(&data_written_cond_);
+ pthread_mutex_unlock(&shared_state_lock_);
+}
+
+int64_t CompressorIOJavaScriptStream::Read(int64_t bytes_to_read,
+ char* destination_buffer) {
+ pthread_mutex_lock(&shared_state_lock_);
+
+ destination_buffer_ = destination_buffer;
+ requestor_->ReadFileChunkRequest(bytes_to_read);
+
+ while (!available_data_) {
+ pthread_cond_wait(&available_data_cond_, &shared_state_lock_);
+ }
+
+ int64_t read_bytes = read_bytes_;
+ available_data_ = false;
+ pthread_mutex_unlock(&shared_state_lock_);
+ return read_bytes;
+}
+
+void CompressorIOJavaScriptStream::ReadFileChunkDone(int64_t read_bytes,
+ pp::VarArrayBuffer* array_buffer) {
+ pthread_mutex_lock(&shared_state_lock_);
+
+ // JavaScript sets a negative value in read_bytes if an error occurred while
+ // reading a chunk.
+ if (read_bytes >= 0) {
+ char* array_buffer_data = static_cast<char*>(array_buffer->Map());
+ memcpy(destination_buffer_, array_buffer_data, read_bytes);
+ array_buffer->Unmap();
+ }
+
+ read_bytes_ = read_bytes;
+ available_data_ = true;
+ pthread_cond_signal(&available_data_cond_);
+ pthread_mutex_unlock(&shared_state_lock_);
+}
diff --git a/unpacker/cpp/compressor_io_javascript_stream.h b/unpacker/cpp/compressor_io_javascript_stream.h
new file mode 100644
index 0000000..d0a22d8
--- /dev/null
+++ b/unpacker/cpp/compressor_io_javascript_stream.h
@@ -0,0 +1,63 @@
+// Copyright 2017 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPRESSOR_IO_JAVSCRIPT_STREAM_H_
+#define COMPRESSOR_IO_JAVSCRIPT_STREAM_H_
+
+#include <pthread.h>
+#include <string>
+
+#include "archive.h"
+#include "ppapi/cpp/instance_handle.h"
+#include "ppapi/cpp/var_array_buffer.h"
+#include "ppapi/utility/completion_callback_factory.h"
+#include "ppapi/utility/threading/lock.h"
+#include "ppapi/utility/threading/simple_thread.h"
+
+#include "compressor_stream.h"
+#include "javascript_compressor_requestor_interface.h"
+
+class CompressorIOJavaScriptStream : public CompressorStream {
+ public:
+ CompressorIOJavaScriptStream(
+ JavaScriptCompressorRequestorInterface* requestor);
+
+ virtual ~CompressorIOJavaScriptStream();
+
+ virtual int64_t Write(int64_t bytes_to_read,
+ const pp::VarArrayBuffer& buffer);
+
+ virtual void WriteChunkDone(int64_t write_bytes);
+
+ virtual int64_t Read(int64_t bytes_to_read, char* destination_buffer);
+
+ virtual void ReadFileChunkDone(int64_t read_bytes,
+ pp::VarArrayBuffer* buffer);
+
+private:
+ // A requestor that makes calls to JavaScript to read and write chunks.
+ JavaScriptCompressorRequestorInterface* requestor_;
+
+ pthread_mutex_t shared_state_lock_;
+ pthread_cond_t available_data_cond_;
+ pthread_cond_t data_written_cond_;
+
+ // The bytelength of the data written onto the archive for the last write
+ // chunk request. If this value is negative, some error occurred when writing
+ // a chunk in JavaScript.
+ int64_t written_bytes_;
+
+ // The bytelength of the data read from the entry for the last read file chunk
+ // request. If this value is negative, some error occurred when reading a
+ // chunk in JavaScript.
+ int64_t read_bytes_;
+
+ // True if destination_buffer_ is available.
+ bool available_data_;
+
+ // Stores the data read from JavaScript.
+ char* destination_buffer_;
+};
+
+#endif // COMPRESSOR_IO_JAVSCRIPT_STREAM_H_
diff --git a/unpacker/cpp/compressor_stream.h b/unpacker/cpp/compressor_stream.h
new file mode 100644
index 0000000..de1f420
--- /dev/null
+++ b/unpacker/cpp/compressor_stream.h
@@ -0,0 +1,42 @@
+// Copyright 2017 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPRESSOR_STREAM_H_
+#define COMPRESSOR_STREAM_H_
+
+#include <string>
+
+// A IO class that reads and writes data from and to files through JavaScript.
+// Currently, Read() and Write() methods are not called at the same time.
+// To improve the performance, these should be called in parallel by preparing
+// two buffers and calling them with these buffers alternatively.
+class CompressorStream {
+ public:
+ virtual ~CompressorStream() {}
+
+ // Writes the given buffer onto the archive. After sending a write chunk
+ // request to JavaScript, it waits until WriteChunkDone() is called in the
+ // main thread. Thus, This method must not be called in the main thread.
+ virtual int64_t Write(int64_t bytes_to_write,
+ const pp::VarArrayBuffer& buffer) = 0;
+
+ // Called when write chunk done response arrives from JavaScript. Sends a
+ // signal to invoke Write function in another thread again.
+ virtual void WriteChunkDone(int64_t write_bytes) = 0;
+
+ // Reads a file chunk from the entry that is currently being processed. After
+ // sending a read file chunk request to JavaScript, it waits until
+ // ReadFileChunkDone() is called in the main thread. Thus, This method must
+ // not be called in the main thread.
+ virtual int64_t Read(int64_t bytes_to_read, char* destination_buffer) = 0;
+
+ // Called when read file chunk done response arrives from JavaScript. Copies
+ // the binary data in the given buffer to destination_buffer_ and Sends a
+ // signal to invoke Read function in another thread again. buffer must not be
+ // const because buffer.Map() and buffer.Unmap() can not be called with const.
+ virtual void ReadFileChunkDone(int64_t read_bytes,
+ pp::VarArrayBuffer* buffer) = 0;
+};
+
+#endif // COMPRESSOR_STREAM_H_
diff --git a/unpacker/cpp/javascript_compressor_requestor_interface.h b/unpacker/cpp/javascript_compressor_requestor_interface.h
new file mode 100644
index 0000000..367c021
--- /dev/null
+++ b/unpacker/cpp/javascript_compressor_requestor_interface.h
@@ -0,0 +1,21 @@
+// Copyright 2017 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef JAVASCRIPT_COMPRESSOR_REQUESTOR_INTERFACE_H_
+#define JAVASCRIPT_COMPRESSOR_REQUESTOR_INTERFACE_H_
+
+#include <string>
+
+// Makes requests from Compressor to JavaScript.
+class JavaScriptCompressorRequestorInterface {
+ public:
+ virtual ~JavaScriptCompressorRequestorInterface() {}
+
+ virtual void WriteChunkRequest(int64_t length,
+ const pp::VarArrayBuffer& buffer) = 0;
+
+ virtual void ReadFileChunkRequest(int64_t length) = 0;
+};
+
+#endif // JAVASCRIPT_COMPRESSOR_REQUESTOR_INTERFACE_H_
diff --git a/unpacker/cpp/javascript_message_sender_interface.h b/unpacker/cpp/javascript_message_sender_interface.h
index 282356b..17c9f9e 100644
--- a/unpacker/cpp/javascript_message_sender_interface.h
+++ b/unpacker/cpp/javascript_message_sender_interface.h
@@ -16,6 +16,9 @@
const std::string& request_id,
const std::string& message) = 0;
+ virtual void SendCompressorError(int compressor_id,
+ const std::string& message) = 0;
+
virtual void SendFileChunkRequest(const std::string& file_system_id,
const std::string& request_id,
int64_t offset,
@@ -46,6 +49,19 @@
int src_line,
const std::string& src_func,
const std::string& message) = 0;
+
+ virtual void SendCreateArchiveDone(int compressor_id) = 0;
+
+ virtual void SendReadFileChunk(int compressor_id_,
+ int64_t file_size) = 0;
+
+ virtual void SendWriteChunk(int compressor_id,
+ const pp::VarArrayBuffer& array_buffer,
+ int64_t length) = 0;
+
+ virtual void SendAddToArchiveDone(int compressor_id) = 0;
+
+ virtual void SendCloseArchiveDone(int compressor_id) = 0;
};
#define CONSOLE_LOG(fsid, rid, msg) \
diff --git a/unpacker/cpp/module.cc b/unpacker/cpp/module.cc
index c5fae8c..24ae6c0 100644
--- a/unpacker/cpp/module.cc
+++ b/unpacker/cpp/module.cc
@@ -13,12 +13,14 @@
#include "ppapi/utility/threading/lock.h"
#include "ppapi/utility/threading/simple_thread.h"
+#include "compressor.h"
#include "request.h"
#include "volume.h"
namespace {
typedef std::map<std::string, Volume*>::const_iterator volume_iterator;
+typedef std::map<int, Compressor*>::const_iterator compressor_iterator;
// An internal implementation of JavaScriptMessageSenderInterface. This class
// handles all communication from the module to the JavaScript code. Thread
@@ -37,6 +39,12 @@
request::CreateFileSystemError(file_system_id, request_id, message));
}
+ virtual void SendCompressorError(int compressor_id,
+ const std::string& message) {
+ JavaScriptPostMessage(
+ request::CreateCompressorError(compressor_id, message));
+ }
+
virtual void SendFileChunkRequest(const std::string& file_system_id,
const std::string& request_id,
int64_t offset,
@@ -91,6 +99,33 @@
file_system_id, request_id, src_file, src_line, src_func, message));
}
+ virtual void SendCreateArchiveDone(int compressor_id) {
+ JavaScriptPostMessage(request::CreateCreateArchiveDoneResponse(
+ compressor_id));
+ }
+
+ virtual void SendReadFileChunk(int compressor_id, int64_t length) {
+ JavaScriptPostMessage(request::CreateReadFileChunkRequest(
+ compressor_id, length));
+ }
+
+ virtual void SendWriteChunk(int compressor_id,
+ const pp::VarArrayBuffer& array_buffer,
+ int64_t length) {
+ JavaScriptPostMessage(request::CreateWriteChunkRequest(
+ compressor_id, array_buffer, length));
+ }
+
+ virtual void SendAddToArchiveDone(int compressor_id) {
+ JavaScriptPostMessage(request::CreateAddToArchiveDoneResponse(
+ compressor_id));
+ }
+
+ virtual void SendCloseArchiveDone(int compressor_id) {
+ JavaScriptPostMessage(request::CreateCloseArchiveDoneResponse(
+ compressor_id));
+ }
+
private:
// Posts a message to JavaScript. This is prone to races in case of using
// NaCl instead of PNaCl. See crbug.com/413513.
@@ -128,6 +163,18 @@
PP_DCHECK(var_dict.Get(request::key::kOperation).is_int());
int operation = var_dict.Get(request::key::kOperation).AsInt();
+ if (request::IsPackRequest(operation))
+ HandlePackMessage(var_dict, operation);
+ else
+ HandleUnpackMessage(var_dict, operation);
+ }
+
+ private:
+
+ // Processes unpack messages.
+ void HandleUnpackMessage(const pp::VarDictionary& var_dict,
+ const int operation) {
+
PP_DCHECK(var_dict.Get(request::key::kFileSystemId).is_string());
std::string file_system_id =
var_dict.Get(request::key::kFileSystemId).AsString();
@@ -135,7 +182,7 @@
PP_DCHECK(var_dict.Get(request::key::kRequestId).is_string());
std::string request_id = var_dict.Get(request::key::kRequestId).AsString();
- // Process operation.
+ // Processes operation.
switch (operation) {
case request::READ_METADATA: {
ReadMetadata(var_dict, file_system_id, request_id);
@@ -183,7 +230,44 @@
}
}
- private:
+ // Processes pack messages.
+ void HandlePackMessage(const pp::VarDictionary& var_dict,
+ const int operation) {
+ PP_DCHECK(var_dict.Get(request::key::kCompressorId).is_int());
+ int compressor_id =
+ var_dict.Get(request::key::kCompressorId).AsInt();
+
+ switch (operation) {
+ case request::CREATE_ARCHIVE: {
+ CreateArchive(compressor_id);
+ break;
+ }
+
+ case request::ADD_TO_ARCHIVE: {
+ AddToArchive(var_dict, compressor_id);
+ break;
+ }
+
+ case request::READ_FILE_CHUNK_DONE: {
+ ReadFileChunkDone(var_dict, compressor_id);
+ break;
+ }
+
+ case request::WRITE_CHUNK_DONE: {
+ WriteChunkDone(var_dict, compressor_id);
+ break;
+ }
+
+ case request::CLOSE_ARCHIVE: {
+ CloseArchive(var_dict, compressor_id);
+ break;
+ }
+
+ default:
+ PP_NOTREACHED();
+ }
+ }
+
// Reads the metadata for the corresponding volume for file_system_id. This
// should be called only once and before any other operation like OpenFile,
// ReadFile, etc.
@@ -320,10 +404,63 @@
iterator->second->ReadFile(request_id, var_dict);
}
+ // Requests libarchive to create an archive object for the given compressor_id.
+ void CreateArchive(int compressor_id) {
+ Compressor* compressor =
+ new Compressor(instance_handle_, compressor_id, &message_sender_);
+ if (!compressor->Init()) {
+ std::stringstream ss;
+ ss << compressor_id;
+ message_sender_.SendCompressorError(
+ compressor_id,
+ "Could not create a compressor for compressor id: " + ss.str() + ".");
+ delete compressor;
+ return;
+ }
+ compressors_[compressor_id] = compressor;
+
+ compressor->CreateArchive();
+ }
+
+ void AddToArchive(const pp::VarDictionary& var_dict,
+ int compressor_id) {
+ compressor_iterator iterator = compressors_.find(compressor_id);
+ PP_DCHECK(iterator != compressors_.end());
+
+ iterator->second->AddToArchive(var_dict);
+ }
+
+ void ReadFileChunkDone(const pp::VarDictionary& var_dict,
+ const int compressor_id) {
+ compressor_iterator iterator = compressors_.find(compressor_id);
+ PP_DCHECK(iterator != compressors_.end());
+
+ iterator->second->ReadFileChunkDone(var_dict);
+ }
+
+ void WriteChunkDone(const pp::VarDictionary& var_dict,
+ int compressor_id) {
+ compressor_iterator iterator = compressors_.find(compressor_id);
+ PP_DCHECK(iterator != compressors_.end());
+
+ iterator->second->WriteChunkDone(var_dict);
+ }
+
+ void CloseArchive(const pp::VarDictionary& var_dict,
+ int compressor_id) {
+ compressor_iterator iterator = compressors_.find(compressor_id);
+
+ if (iterator != compressors_.end())
+ iterator->second->CloseArchive(var_dict);
+ }
+
// A map that holds for every opened archive its instance. The key is the file
// system id of the archive.
std::map<std::string, Volume*> volumes_;
+ // A map from compressor ids to compressors.
+ std::map<int, Compressor*> compressors_;
+
// An pp::InstanceHandle used to create pp::SimpleThread in Volume.
pp::InstanceHandle instance_handle_;
diff --git a/unpacker/cpp/request.cc b/unpacker/cpp/request.cc
index 443e9bf..db8528b 100644
--- a/unpacker/cpp/request.cc
+++ b/unpacker/cpp/request.cc
@@ -21,6 +21,12 @@
} // namespace
+// Return true if the given operation is related to packing.
+bool request::IsPackRequest(int operation) {
+ return request::MINIMUM_PACK_REQUEST_VALUE <= operation ||
+ operation == request::COMPRESSOR_ERROR;
+}
+
pp::VarDictionary request::CreateReadMetadataDoneResponse(
const std::string& file_system_id,
const std::string& request_id,
@@ -85,6 +91,56 @@
return response;
}
+pp::VarDictionary request::CreateCreateArchiveDoneResponse(
+ const int compressor_id) {
+ pp::VarDictionary request;
+ request.Set(request::key::kOperation, CREATE_ARCHIVE_DONE);
+ request.Set(request::key::kCompressorId, compressor_id);
+ return request;
+}
+
+pp::VarDictionary request::CreateReadFileChunkRequest(
+ const int compressor_id,
+ const int64_t length) {
+ pp::VarDictionary request;
+ request.Set(request::key::kOperation, READ_FILE_CHUNK);
+ request.Set(request::key::kCompressorId, compressor_id);
+
+ std::stringstream ss_length;
+ ss_length << length;
+ request.Set(request::key::kLength, ss_length.str());
+ return request;
+}
+
+pp::VarDictionary request::CreateWriteChunkRequest(
+ int compressor_id,
+ const pp::VarArrayBuffer& array_buffer,
+ int64_t length) {
+ pp::VarDictionary request;
+ request.Set(request::key::kOperation, WRITE_CHUNK);
+ request.Set(request::key::kCompressorId, compressor_id);
+
+ request.Set(request::key::kChunkBuffer, array_buffer);
+ std::stringstream ss_length;
+ ss_length << length;
+ request.Set(request::key::kLength, ss_length.str());
+ return request;
+}
+
+pp::VarDictionary request::CreateAddToArchiveDoneResponse(int compressor_id) {
+ pp::VarDictionary request;
+ request.Set(request::key::kOperation, ADD_TO_ARCHIVE_DONE);
+ request.Set(request::key::kCompressorId, compressor_id);
+ return request;
+}
+
+pp::VarDictionary request::CreateCloseArchiveDoneResponse(int compressor_id) {
+ pp::VarDictionary request;
+ request.Set(request::key::kOperation, CLOSE_ARCHIVE_DONE);
+ request.Set(request::key::kCompressorId, compressor_id);
+ return request;
+}
+
pp::VarDictionary request::CreateFileSystemError(
const std::string& file_system_id,
const std::string& request_id,
@@ -111,6 +167,16 @@
return request;
}
+pp::VarDictionary request::CreateCompressorError(
+ int compressor_id,
+ const std::string& error) {
+ pp::VarDictionary request;
+ request.Set(request::key::kOperation, COMPRESSOR_ERROR);
+ request.Set(request::key::kCompressorId, compressor_id);
+ request.Set(request::key::kError, error);
+ return request;
+}
+
int64_t request::GetInt64FromString(const pp::VarDictionary& dictionary,
const std::string& request_key) {
std::stringstream ss_int64(dictionary.Get(request_key).AsString());
diff --git a/unpacker/cpp/request.h b/unpacker/cpp/request.h
index e24225c..197866c 100644
--- a/unpacker/cpp/request.h
+++ b/unpacker/cpp/request.h
@@ -16,21 +16,15 @@
// on the JS side.
namespace key {
-// Mandatory keys for all requests.
+// Mandatory keys for all unpacking requests.
const char kOperation[] = "operation"; // Should be a request::Operation.
const char kFileSystemId[] = "file_system_id"; // Should be a string.
const char kRequestId[] = "request_id"; // Should be a string.
-// Optional keys depending on request operation.
-const char kError[] = "error"; // Should be a string.
+// Optional keys unique to unpacking operations.
const char kMetadata[] = "metadata"; // Should be a pp:VarDictionary.
const char kArchiveSize[] =
"archive_size"; // Should be a string as int64_t is not support by pp::Var.
-const char kChunkBuffer[] = "chunk_buffer"; // Should be a pp::VarArrayBuffer.
-const char kOffset[] = "offset"; // Should be a string as int64_t is not
- // supported by pp::Var.
-const char kLength[] = "length"; // Should be a string as int64_t is not
- // supported by pp::Var.
const char kIndex[] = "index"; // Should be a string as int64_t is not
// supported by pp::Var.
const char kEncoding[] = "encoding"; // Should be a string.
@@ -40,11 +34,30 @@
// pp::VarArrayBuffer.
const char kHasMoreData[] = "has_more_data"; // Should be a bool.
const char kPassphrase[] = "passphrase"; // Should be a string.
+
+// Mandatory keys for all packing requests.
+const char kCompressorId[] = "compressor_id"; // Should be an int.
+
+// Optional keys unique to packing operations.
+const char kEntryId[] = "entry_id"; // Should be an int.
+const char kPathname[] = "pathname"; // Should be a string.
+const char kFileSize[] = "file_size"; // Should be a string.
+const char kIsDirectory[] = "is_directory"; // Should be a bool.
+const char kModificationTime[] = "modification_time"; // Should be a string
+ // (mm/dd/yy h:m:s).
+const char kHasError[] = "has_error"; // Should be a bool.
+
+// Optional keys used for both packing and unpacking operations.
+const char kError[] = "error"; // Should be a string.
+const char kChunkBuffer[] = "chunk_buffer"; // Should be a pp::VarArrayBuffer.
+const char kOffset[] = "offset"; // Should be a string as int64_t is not
+ // supported by pp::Var.
+const char kLength[] = "length"; // Should be a string as int64_t is not
+ // supported by pp::Var.
const char kSrcFile[] = "src_file"; // Should be a string.
const char kSrcLine[] = "src_line"; // Should be a string.
const char kSrcFunc[] = "src_func"; // Should be a string.
const char kMessage[] = "message"; // Should be a string.
-
} // namespace key
// Defines request operations. These operations should be the same as the
@@ -67,9 +80,26 @@
READ_FILE_DONE = 14,
CONSOLE_LOG = 15,
CONSOLE_DEBUG = 16,
+ CREATE_ARCHIVE = 17,
+ CREATE_ARCHIVE_DONE = 18,
+ ADD_TO_ARCHIVE = 19,
+ ADD_TO_ARCHIVE_DONE = 20,
+ READ_FILE_CHUNK = 21,
+ READ_FILE_CHUNK_DONE = 22,
+ WRITE_CHUNK = 23,
+ WRITE_CHUNK_DONE = 24,
+ CLOSE_ARCHIVE = 25,
+ CLOSE_ARCHIVE_DONE = 26,
FILE_SYSTEM_ERROR = -1, // Errors specific to a file system.
+ COMPRESSOR_ERROR = -2 // Errors specific to a compressor.
};
+// Operations greater than or equal to this value are for packing.
+const int MINIMUM_PACK_REQUEST_VALUE = 17;
+
+// Return true if the given operation is related to packing.
+bool IsPackRequest(int operation);
+
// Creates a response to READ_METADATA request.
pp::VarDictionary CreateReadMetadataDoneResponse(
const std::string& file_system_id,
@@ -103,6 +133,19 @@
const pp::VarArrayBuffer& array_buffer,
bool has_more_data);
+pp::VarDictionary CreateCreateArchiveDoneResponse(int compressor_id);
+
+pp::VarDictionary CreateReadFileChunkRequest(int compressor_id,
+ int64_t length);
+
+pp::VarDictionary CreateWriteChunkRequest(int compressor_id,
+ const pp::VarArrayBuffer& array_buffer,
+ int64_t length);
+
+pp::VarDictionary CreateAddToArchiveDoneResponse(int compressor_id);
+
+pp::VarDictionary CreateCloseArchiveDoneResponse(int compressor_id);
+
// Creates a file system error.
pp::VarDictionary CreateFileSystemError(const std::string& file_system_id,
const std::string& request_id,
@@ -116,6 +159,10 @@
const std::string& src_func,
const std::string& message);
+// Creates a compressor error.
+pp::VarDictionary CreateCompressorError(int compressor_id,
+ const std::string& error);
+
// Obtains a int64_t from a string value inside dictionary based on a
// request::Key.
int64_t GetInt64FromString(const pp::VarDictionary& dictionary,
diff --git a/unpacker/cpp/volume.cc b/unpacker/cpp/volume.cc
index 8d5a3f5..588698f 100644
--- a/unpacker/cpp/volume.cc
+++ b/unpacker/cpp/volume.cc
@@ -391,7 +391,7 @@
return;
}
- if (!volume_archive_->GetNextHeader()) {
+ if (volume_archive_->GetNextHeader() == VolumeArchive::RESULT_FAIL) {
message_sender_->SendFileSystemError(
file_system_id_, args.request_id, volume_archive_->error_message());
ClearJob();
diff --git a/unpacker/cpp/volume.h b/unpacker/cpp/volume.h
index 212b336..dc8efb4 100644
--- a/unpacker/cpp/volume.h
+++ b/unpacker/cpp/volume.h
@@ -11,9 +11,9 @@
#include "ppapi/cpp/instance_handle.h"
#include "ppapi/cpp/var_array_buffer.h"
#include "ppapi/cpp/var_dictionary.h"
+#include "ppapi/utility/completion_callback_factory.h"
#include "ppapi/utility/threading/lock.h"
#include "ppapi/utility/threading/simple_thread.h"
-#include "ppapi/utility/completion_callback_factory.h"
#include "javascript_requestor_interface.h"
#include "javascript_message_sender_interface.h"
diff --git a/unpacker/html/compressor.html b/unpacker/html/compressor.html
new file mode 100644
index 0000000..f245e0d
--- /dev/null
+++ b/unpacker/html/compressor.html
@@ -0,0 +1,6 @@
+<!doctype html>
+<html>
+ <head>
+ <script src="../js/compressor-foreground.js"></script>
+ </head>
+</html>
diff --git a/unpacker/js/app.js b/unpacker/js/app.js
index f1ea470..21182a1 100644
--- a/unpacker/js/app.js
+++ b/unpacker/js/app.js
@@ -47,6 +47,13 @@
volumes: {},
/**
+ * Each "Pack with" request from Files app creates a new compressor.
+ * Thus, multiple compressors can exist at the same time.
+ * @type {!Object<!unpacker.types.CompressorId, !unpacker.Compressor>}
+ */
+ compressors: {},
+
+ /**
* A map with promises of loading a volume's metadata from NaCl.
* Any call from fileSystemProvider API should work only on valid metadata.
* These promises ensure that the fileSystemProvider API calls wait for the
@@ -79,15 +86,30 @@
/**
* Function called on receiving a message from NaCl module. Registered by
* common.js.
+ * Process pack message by getting compressor and passing the message to it.
* @param {!Object} message The message received from NaCl module.
+ * @param {!unpacker.request.Operation} operation
* @private
*/
- handleMessage_: function(message) {
- // Get mandatory fields in a message.
- var operation = message.data[unpacker.request.Key.OPERATION];
- console.assert(operation != undefined, // Operation can be 0.
- 'No NaCl operation: ' + operation + '.');
+ handlePackMessage_: function(message, operation) {
+ var compressorId = message.data[unpacker.request.Key.COMPRESSOR_ID];
+ console.assert(compressorId, 'No NaCl compressor id.');
+ var compressor = unpacker.app.compressors[compressorId];
+ if (!compressor) {
+ console.error('No compressor for compressor id: ' + compressorId + '.');
+ return;
+ }
+ compressor.processMessage(message.data, operation);
+ },
+
+ /**
+ * Process unpack message by getting volume and passing the message to it.
+ * @param {!Object} message The message received from NaCl module.
+ * @param {!unpacker.request.Operation} operation
+ * @private
+ */
+ handleUnpackMessage_: function(message, operation) {
var fileSystemId = message.data[unpacker.request.Key.FILE_SYSTEM_ID];
console.assert(fileSystemId, 'No NaCl file system id.');
@@ -106,6 +128,25 @@
},
/**
+ * Function called on receiving a message from NaCl module. Registered by
+ * common.js.
+ * @param {!Object} message The message received from NaCl module.
+ * @private
+ */
+ handleMessage_: function(message) {
+ // Get mandatory fields in a message.
+ var operation = message.data[unpacker.request.Key.OPERATION];
+ console.assert(operation != undefined, // Operation can be 0.
+ 'No NaCl operation: ' + operation + '.');
+
+ // Assign the message to either module.
+ if (unpacker.request.isPackRequest(operation))
+ unpacker.app.handlePackMessage_(message, operation);
+ else
+ unpacker.app.handleUnpackMessage_(message, operation);
+ },
+
+ /**
* Saves state in case of restarts, event page suspend, crashes, etc.
* @param {!Array<!unpacker.types.FileSystemId>} fileSystemIdsArray
* @private
@@ -421,6 +462,36 @@
},
/**
+ * Cleans up the resources for a compressor.
+ * @param {!unpacker.types.CompressorId} compressorId
+ * @param {boolean} hasError
+ */
+ cleanupCompressor: function(compressorId, hasError) {
+ var compressor = unpacker.app.compressors[compressorId];
+ if (!compressor) {
+ console.error('No compressor for: compressor id' + compressorId + '.');
+ return;
+ }
+
+ unpacker.app.mountProcessCounter--;
+ if (Object.keys(unpacker.app.volumes).length === 0 &&
+ unpacker.app.mountProcessCounter === 0) {
+ unpacker.app.unloadNaclModule();
+ } else {
+ // Request libarchive to abort any ongoing process and release resources.
+ // The argument indicates whether an error occurred or not.
+ if (hasError)
+ compressor.sendCloseArchiveRequest(hasError);
+ }
+
+ // Delete the archive file if it exists.
+ if (compressor.archiveFileEntry)
+ compressor.archiveFileEntry.remove();
+
+ delete unpacker.app.compressors[compressorId];
+ },
+
+ /**
* Unmounts a volume and removes any resources related to the volume from both
* the extension and the local storage state.
* @param {!unpacker.types.FileSystemId} fileSystemId
@@ -567,6 +638,66 @@
},
/**
+ * Creates a new compressor and compresses entries.
+ * @param {!Object} launchData
+ */
+ onLaunchedWithPack: function(launchData) {
+ unpacker.app.mountProcessCounter++;
+
+ // Create a promise to load the NaCL module.
+ if (!unpacker.app.moduleLoadedPromise) {
+ unpacker.app.loadNaclModule(unpacker.app.DEFAULT_MODULE_NMF,
+ unpacker.app.DEFAULT_MODULE_TYPE);
+ }
+
+ unpacker.app.moduleLoadedPromise
+ .then(function() {
+ var compressor = new unpacker.Compressor(
+ /** @type {!Object} */ (unpacker.app.naclModule),
+ launchData.items);
+
+ var compressorId = compressor.getCompressorId();
+
+ unpacker.app.compressors[compressorId] = compressor;
+
+ // TODO(takise): Error messages have not been prepared yet for timer
+ // and error processing.
+
+ // If packing takes significant amount of time, then show a
+ // notification about packing in progress.
+ // var deferredNotificationTimer = setTimeout(function() {
+ // chrome.notifications.create(compressorId.toString(), {
+ // type: 'basic',
+ // iconUrl: chrome.runtime.getManifest().icons[128],
+ // title: entry.name,
+ // message: chrome.i18n.getMessage('packingMessage'),
+ // }, function() {});
+ // }, unpacker.app.PACKING_NOTIFICATION_DELAY);
+
+ var onError = function(compressorId) {
+ // clearTimeout(deferredNotificationTimer);
+ // console.error('Packing error: ' + error.message + '.');
+ // chrome.notifications.create(compressorId.toString(), {
+ // type: 'basic',
+ // iconUrl: chrome.runtime.getManifest().icons[128],
+ // title: entry.name,
+ // message: chrome.i18n.getMessage('packingErrorMessage')
+ // }, function() {});
+ unpacker.app.cleanupCompressor(compressorId, true /* hasError */);
+ };
+
+ var onSuccess = function(compressorId) {
+ // clearTimeout(deferredNotificationTimer);
+ // chrome.notifications.clear(compressorId.toString(),
+ // function() {});
+ unpacker.app.cleanupCompressor(compressorId, false /* hasError */);
+ };
+
+ compressor.compress(onSuccess, onError);
+ });
+ },
+
+ /**
* Creates a volume for every opened file with the extension or mime type
* declared in the manifest file.
* @param {!Object} launchData
@@ -579,13 +710,7 @@
* system id of the volume that failed to load. Can be called multiple
* times, depending on how many volumes must be loaded.
*/
- onLaunched: function(launchData, opt_onSuccess, opt_onError) {
- if (launchData.items == null) {
- // The user tried to launch us directly.
- console.log('Ignoring launch request w/out items field', {launchData});
- return;
- }
-
+ onLaunchedWithUnpack: function(launchData, opt_onSuccess, opt_onError) {
// Increment the counter that indicates the number of ongoing mouot process.
unpacker.app.mountProcessCounter++;
@@ -684,6 +809,28 @@
},
/**
+ * Fired when this extension is launched.
+ * Calls a module designated by launchData.id.
+ * Currently, Verbs API does not support "unpack" option. Thus, any launchData
+ * that does not have "pack" as id is regarded as unpack for now.
+ * @param {!Object} launchData
+ * @param {function(string)=} opt_onSuccess
+ * @param {function(string)=} opt_onError
+ */
+ onLaunched: function(launchData, opt_onSuccess, opt_onError) {
+ if (launchData.items == null) {
+ // The user tried to launch us directly.
+ console.log('Ignoring launch request w/out items field', {launchData});
+ return;
+ }
+
+ if (launchData.id === "pack")
+ unpacker.app.onLaunchedWithPack(launchData);
+ else
+ unpacker.app.onLaunchedWithUnpack(launchData, opt_onSuccess, opt_onError);
+ },
+
+ /**
* Saves the state before suspending the event page, so we can resume it
* once new events arrive.
*/
diff --git a/unpacker/js/compressor-foreground.js b/unpacker/js/compressor-foreground.js
new file mode 100644
index 0000000..bf482e3
--- /dev/null
+++ b/unpacker/js/compressor-foreground.js
@@ -0,0 +1,39 @@
+// Copyright 2017 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+window.onload = function() {
+ chrome.runtime.getBackgroundPage(function(backgroundPage) {
+
+ // Called from the background page. We need to place this function here
+ // because chrome.fileSystem.chooseEntry only works on forground page.
+ backgroundPage.unpacker.Compressor.prototype.createArchiveFileForeground_ =
+ function(compressorId) {
+ var compressor =
+ backgroundPage.unpacker.app.compressors[compressorId];
+ var suggestedName = compressor.archiveName_;
+ // Create an archive file.
+ chrome.fileSystem.chooseEntry(
+ {type: 'saveFile', suggestedName: suggestedName},
+ function(entry, fileEntries) {
+ if (!entry) {
+ console.error('Failed to create an archive file.');
+ compressor.onError_(compressor.compressorId_);
+ return;
+ }
+
+ compressor.archiveFileEntry_ = entry;
+
+ compressor.sendCreateArchiveRequest_();
+ });
+ };
+
+ // Some compressors are waiting for this foreground page to be loaded.
+ backgroundPage.unpacker.Compressor.CompressorIdQueue.forEach(
+ function(compressorId) {
+ var compressor =
+ backgroundPage.unpacker.app.compressors[compressorId];
+ compressor.createArchiveFileForeground_(compressorId);
+ });
+ });
+};
diff --git a/unpacker/js/compressor.js b/unpacker/js/compressor.js
new file mode 100644
index 0000000..a96fd4d
--- /dev/null
+++ b/unpacker/js/compressor.js
@@ -0,0 +1,516 @@
+// Copyright 2017 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+'use strict';
+
+/**
+ * A class that takes care of communication with NaCL and creates an archive.
+ * One instance of this class is created for each pack request. Since multiple
+ * compression requests can be in progress at the same time, each instance has
+ * a unique compressor id of positive integer. Every communication with NaCL
+ * must be done with compressor id.
+ * @constructor
+ * @param {!Object} naclModule The nacl module.
+ * @param {!Array} items The items to be packed.
+ */
+unpacker.Compressor = function(naclModule, items) {
+ /**
+ * @private {!Object}
+ * @const
+ */
+ this.naclModule_ = naclModule;
+
+ /**
+ * @private {!Array}
+ * @const
+ */
+ this.items_ = items;
+
+ /**
+ * @private {!unpacker.types.CompressorId}
+ * @const
+ */
+ this.compressorId_ = unpacker.Compressor.compressorIdCounter++;
+
+ /**
+ * @private {string}
+ * @const
+ */
+ this.archiveName_ = this.getArchiveName_();
+
+ /**
+ * The counter used to assign a unique id to each entry.
+ * @type {number}
+ */
+ this.entryIdCounter_ = 1;
+
+ /**
+ * The set of entry ids waiting for metadata from FileSystem API.
+ * These requests needs to be tracked here to tell whether all pack process
+ * has finished or not.
+ * @type {!Set}
+ */
+ this.metadataRequestsInProgress_ = new Set();
+
+ /**
+ * The queue containing entry ids that have already obtained metadata from
+ * FileSystem API and are waiting to be added into archive.
+ * @type {!Array}
+ */
+ this.pendingAddToArchiveRequests_ = [];
+
+ /**
+ * The id of the entry that is being compressed and written into archive.
+ * Note that packing of each entry should be done one by one unlike
+ * unpacking. Thus, at most one entry is processed at once.
+ * @type {!unpacker.types.EntryId}
+ */
+ this.entryIdInProgress_ = 0;
+
+ /**
+ * Map from entry ids to entries.
+ * @const {!Object<!unpacker.types.EntryId, !FileEntry|!DirectoryEntry>}
+ */
+ this.entries_ = {};
+
+ /**
+ * Map from entry ids to its metadata.
+ * @const {!Object<!unpacker.types.EntryId, !Metadata>}
+ */
+ this.metadata_ = {};
+
+ /**
+ * The offset from which the entry in progress should be read.
+ * @type {number}
+ */
+ this.offset_ = 0;
+};
+
+/**
+ * The counter which is assigned and incremented every time a new compressor
+ * instance is created.
+ * @type {number}
+ */
+unpacker.Compressor.compressorIdCounter = 1;
+
+/**
+ * The queue containing compressor ids that wait for foreground page to be
+ * loaded. Once this extension becomes a component extension, we don't need to
+ * create an archive file on the foreground page and this also gets unnecessary.
+ * @type {!Array}
+ */
+unpacker.Compressor.CompressorIdQueue = [];
+
+/**
+ * The default archive name.
+ * @type {string}
+ */
+unpacker.Compressor.DEFAULT_ARCHIVE_NAME = 'Archive.zip';
+
+/**
+ * The getter function for compressor id.
+ * @return {!unpacker.types.CompressorId}
+ */
+unpacker.Compressor.prototype.getCompressorId = function() {
+ return this.compressorId_;
+};
+
+/**
+ * Returns the archive file name.
+ * @private
+ * @return {string}
+ */
+unpacker.Compressor.prototype.getArchiveName_ = function() {
+ // When multiple entries are selected.
+ if (this.items_.length !== 1)
+ return unpacker.Compressor.DEFAULT_ARCHIVE_NAME;
+
+ var name = this.items_[0].entry.name
+ var idx = name.lastIndexOf('.');
+ // When the name does not have extension.
+ // TODO(takise): This converts file.tar.gz to file.tar.zip.
+ if (idx === -1)
+ return name + '.zip';
+ // When the name has extension.
+ return name.substring(0, idx) + '.zip';
+};
+
+/**
+ * Starts actual compressing process.
+ * Creates an archive file and requests libarchive to create an archive object.
+ * @param {function(!unpacker.types.CompressorId)} onSuccess
+ * @param {function(!unpacker.types.CompressorId)} onError
+ */
+unpacker.Compressor.prototype.compress = function(onSuccess, onError) {
+ this.onSuccess_ = onSuccess;
+ this.onError_ = onError;
+
+ this.getArchiveFile_();
+};
+
+/**
+ * Gets an archive file with write permission. Currently, this extension does
+ * not have permission to create files from the background page. Thus, this
+ * function first creates a foreground page and then creates an archive file in
+ * it. Once this extension becomes a component extension, this process will be
+ * simpler.
+ * @private
+ */
+unpacker.Compressor.prototype.getArchiveFile_ = function() {
+ // If the foreground page already exists, create an archive file.
+ if (this.createArchiveFileForeground_) {
+ this.createArchiveFileForeground_(this.compressorId_);
+ } else {
+ // If the foreground page does not exist, push the id of this compressor to
+ // the queue so that we can resume later and create the foreground page.
+ // We need this queue because multiple compressors can wait for the
+ // foreground page to be loaded.
+ var queue = unpacker.Compressor.CompressorIdQueue;
+ queue.push(this.compressorId_);
+ if (queue.length === 1) {
+ chrome.app.window.create('../html/compressor.html', {hidden: true});
+ }
+ }
+};
+
+/**
+ * Sends an create archive request to NaCL.
+ * @private
+ */
+unpacker.Compressor.prototype.sendCreateArchiveRequest_ = function() {
+ var request = unpacker.request.createCreateArchiveRequest(
+ this.compressorId_);
+ this.naclModule_.postMessage(request);
+}
+
+/**
+ * A handler of create archive done response.
+ * Enumerates entries and requests FileSystem API for their metadata.
+ * @private
+ */
+unpacker.Compressor.prototype.createArchiveDone_ = function() {
+ this.items_.forEach(function(item) {
+ this.getEntryMetadata_(item.entry);
+ }.bind(this));
+}
+
+/**
+ * Gets metadata of a file or directory.
+ * @param {!FileEntry|!DirectoryEntry} entry FileEntry or DirectoryEntry.
+ * @private
+ */
+unpacker.Compressor.prototype.getEntryMetadata_ = function(entry) {
+ if (entry.isFile)
+ this.getSingleMetadata_(entry);
+ else
+ this.getDirectoryEntryMetadata_(/** @type {!DirectoryEntry} */ (entry));
+}
+
+/**
+ * Requests metadata of an entry non-recursively.
+ * @param {!FileEntry|!DirectoryEntry} entry FileEntry or DirectoryEntry.
+ * @private
+ */
+unpacker.Compressor.prototype.getSingleMetadata_ = function(entry) {
+ var entryId = this.entryIdCounter_++;
+ this.metadataRequestsInProgress_.add(entryId);
+ this.entries_[entryId] = entry;
+
+ entry.getMetadata(function(metadata) {
+ this.metadataRequestsInProgress_.delete(entryId);
+ this.pendingAddToArchiveRequests_.push(entryId);
+ this.metadata_[entryId] = metadata;
+ this.sendAddToArchiveRequest_();
+ }.bind(this), function(error) {
+ console.error('Failed to get metadata: ' +
+ error.message + '.');
+ this.onError_(this.compressorId_);
+ }.bind(this));
+}
+
+/**
+ * Requests metadata of an entry recursively.
+ * @param {!DirectoryEntry} dir DirectoryEntry.
+ * @private
+ */
+unpacker.Compressor.prototype.getDirectoryEntryMetadata_ = function(dir) {
+
+ // Read entries in dir and call getEntryMetadata_ for them recursively.
+ var dirReader = dir.createReader();
+
+ // Recursive function
+ var getEntries = function() {
+ dirReader.readEntries(function(results) {
+ // ReadEntries must be called until it returns nothing, because
+ // it does not necessarily return all entries in the directory.
+ if (results.length) {
+ results.forEach(this.getEntryMetadata_.bind(this));
+ getEntries();
+ }
+ }.bind(this), function(error) {
+ console.error('Failed to get directory entries: ' +
+ error.message + '.');
+ this.onError_(this.compressorId_);
+ }.bind(this));
+ }.bind(this);
+
+ getEntries();
+
+ // Get the metadata of this dir itself.
+ this.getSingleMetadata_(dir);
+}
+
+/**
+ * Pops an entry from the queue and adds it to the archive.
+ * If another entry is in progress, this function does nothing. If there is no
+ * entry in the queue, it shifts to close archive process. Otherwise, this sends
+ * an add to archive request for a popped entry with its metadata to libarchive.
+ * @private
+ */
+unpacker.Compressor.prototype.sendAddToArchiveRequest_ = function() {
+ // Another process is in progress.
+ if (this.entryIdInProgress_ != 0)
+ return;
+
+ // All entries have already been archived.
+ if (this.pendingAddToArchiveRequests_.length === 0) {
+ if (this.metadataRequestsInProgress_.size === 0)
+ this.sendCloseArchiveRequest(false /* hasError */);
+ return;
+ }
+
+ var entryId = this.pendingAddToArchiveRequests_.shift();
+ this.entryIdInProgress_ = entryId;
+
+ // Convert the absolute path on the virtual filesystem to a relative path from
+ // the archive root by removing the leading '/' if exists.
+ var fullPath = this.entries_[entryId].fullPath;
+ if (fullPath.length && fullPath[0] == '/')
+ fullPath = fullPath.substring(1);
+
+ // Modification time is sent as string in a format: 'mm/dd/yy hh:mm:ss'.
+ var mt = this.metadata_[entryId].modificationTime;
+ var formattedTime = (mt.getMonth() + 1) + '/' + mt.getDate() + '/' +
+ mt.getFullYear() + ' ' + mt.getHours() + ':' +
+ mt.getMinutes() + ':' + mt.getSeconds();
+
+ var request = unpacker.request.createAddToArchiveRequest(
+ this.compressorId_, entryId, fullPath,
+ this.metadata_[entryId].size, formattedTime,
+ this.entries_[entryId].isDirectory);
+ this.naclModule_.postMessage(request);
+}
+
+/**
+ * Sends a close archive request to libarchive. libarchive writes metadata of
+ * the archive itself on the archive and releases objects obtainted in the
+ * packing process.
+ */
+unpacker.Compressor.prototype.sendCloseArchiveRequest = function(hasError) {
+ var request = unpacker.request.createCloseArchiveRequest(
+ this.compressorId_, hasError);
+ this.naclModule_.postMessage(request);
+}
+
+/**
+ * Sends a read file chunk done response.
+ * @param {number} length The number of bytes read from the entry.
+ * @param {!ArrayBuffer} buffer A buffer containing the data that was read.
+ * @private
+ */
+unpacker.Compressor.prototype.sendReadFileChunkDone_ =
+ function(length, buffer) {
+ var request = unpacker.request.createReadFileChunkDoneResponse(
+ this.compressorId_, length, buffer);
+ this.naclModule_.postMessage(request);
+}
+
+/**
+ * A handler of read file chunk messages.
+ * Reads 'length' bytes from the entry currently in process.
+ * @param {!Object} data
+ * @private
+ */
+unpacker.Compressor.prototype.onReadFileChunk_ = function(data) {
+ var entryId = this.entryIdInProgress_;
+ var entry = this.entries_[entryId];
+ var length = Number(data[unpacker.request.Key.LENGTH]);
+
+ // A function to create a reader and read bytes.
+ var readFileChunk = function() {
+ var file = this.file_.slice(this.offset_, this.offset_ + length);
+ var reader = new FileReader();
+
+ reader.onloadend = function(event) {
+ var buffer = event.target.result;
+
+ // The buffer must have 'length' bytes because the byte length which can
+ // be read from the file is already calculated on NaCL side.
+ if (buffer.byteLength !== length) {
+ console.error('Tried to read chunk with length ' + length +
+ ', but byte with length ' + buffer.byteLength + ' was returned.');
+
+ // If the first argument(length) is negative, it means that an error
+ // occurred in reading a chunk.
+ this.sendReadFileChunkDone_(-1, buffer);
+ this.onError_(this.compressorId_);
+ return;
+ }
+
+ this.offset_ += length;
+ this.sendReadFileChunkDone_(length, buffer);
+ }.bind(this);
+
+ reader.onerror = function(event) {
+ console.error('Failed to read file chunk. Name: ' + file.name +
+ ', offset: ' + this.offset_ + ', length: ' + length + '.');
+
+ // If the first argument(length) is negative, it means that an error
+ // occurred in reading a chunk.
+ this.sendReadFileChunkDone_(-1, new ArrayBuffer(0));
+ this.onError_(this.compressorId_);
+ }
+
+ reader.readAsArrayBuffer(file);
+ }.bind(this);
+
+ // When the entry is read for the first time.
+ if (!this.file_) {
+ entry.file(function(file) {
+ this.file_ = file;
+ readFileChunk();
+ }.bind(this));
+ return;
+ }
+
+ // From the second time onward.
+ readFileChunk();
+}
+
+/**
+ * A handler of write chunk requests.
+ * Writes the data in the given buffer onto the archive file.
+ * @param {!Object} data
+ * @private
+ */
+unpacker.Compressor.prototype.onWriteChunk_ = function(data) {
+ var length = Number(data[unpacker.request.Key.LENGTH]);
+ var buffer = data[unpacker.request.Key.CHUNK_BUFFER];
+ this.writeChunk_(length, buffer, this.sendWriteChunkDone_.bind(this));
+}
+
+/**
+ * Writes buffer into the archive file (window.archiveFileEntry).
+ * @param {number} length The number of bytes in the buffer to write.
+ * @param {!ArrayBuffer} buffer The buffer to write in the archive.
+ * @param {function(number)} callback Callback to execute at the end of the
+ * function. This function has one parameter: length, which represents the
+ * length of bytes written on to the archive. If writing a chunk fails,
+ * a negative value must be assigned to this argument.
+ * @private
+ */
+unpacker.Compressor.prototype.writeChunk_ = function(length, buffer,
+ callback) {
+ // TODO(takise): Use the same instance of FileWriter over multiple calls of
+ // this function instead of creating new ones.
+ this.archiveFileEntry_.createWriter(function(fileWriter) {
+ fileWriter.onwriteend = function(event) {
+ callback(length);
+ };
+
+ fileWriter.onerror = function(event) {
+ console.error('Failed to write chunk to ' + this.archiveFileEntry_ + '.');
+
+ // If the first argument(length) is negative, it means that an error
+ // occurred in writing a chunk.
+ callback(-1 /* length */);
+ this.onError_(this.compressorId_);
+ };
+
+ // Create a new Blob and append it to the archive file.
+ var blob = new Blob([buffer], {});
+ fileWriter.seek(fileWriter.length);
+ fileWriter.write(blob);
+ }, function(event) {
+ console.error('Failed to create writer for ' + this.archiveFileEntry_ +
+ '.');
+ this.onError_(this.compressorId_);
+ });
+};
+
+/**
+ * Sends a write chunk done response.
+ * @param {number} length The number of bytes written onto the entry.
+ * @private
+ */
+unpacker.Compressor.prototype.sendWriteChunkDone_ = function(length) {
+ var request = unpacker.request.createWriteChunkDoneResponse(
+ this.compressorId_, length);
+ this.naclModule_.postMessage(request);
+}
+
+/**
+ * A handler of add to archive done responses.
+ * Resets information on the current entry and starts processing another entry.
+ * @private
+ */
+unpacker.Compressor.prototype.onAddToArchiveDone_ = function() {
+ // Reset information on the current entry.
+ this.entryIdInProgress_ = 0;
+ this.file_ = null;
+ this.offset_ = 0;
+
+ // Start processing another entry.
+ this.sendAddToArchiveRequest_();
+}
+
+/**
+ * A handler of close archive responses.
+ * Receiving this response means the entire packing process has finished.
+ * @private
+ */
+unpacker.Compressor.prototype.onCloseArchiveDone_ = function() {
+ this.onSuccess_(this.compressorId_);
+}
+
+/**
+ * Processes messages from NaCl module.
+ * @param {!Object} data The data contained in the message from NaCl. Its
+ * types depend on the operation of the request.
+ * @param {!unpacker.request.Operation} operation An operation from request.js.
+ */
+unpacker.Compressor.prototype.processMessage = function(data, operation) {
+ switch (operation) {
+ case unpacker.request.Operation.CREATE_ARCHIVE_DONE:
+ this.createArchiveDone_();
+ break;
+
+ case unpacker.request.Operation.READ_FILE_CHUNK:
+ this.onReadFileChunk_(data);
+ break;
+
+ case unpacker.request.Operation.WRITE_CHUNK:
+ this.onWriteChunk_(data);
+ break;
+
+ case unpacker.request.Operation.ADD_TO_ARCHIVE_DONE:
+ this.onAddToArchiveDone_();
+ break;
+
+ case unpacker.request.Operation.CLOSE_ARCHIVE_DONE:
+ this.onCloseArchiveDone_();
+ break;
+
+ case unpacker.request.Operation.COMPRESSOR_ERROR:
+ console.error('Compressor error for compressor id ' + this.compressorId_ +
+ ': ' + data[unpacker.request.Key.ERROR]); // The error contains
+ // the '.' at the end.
+ this.onError_(this.compressorId_);
+ break;
+
+ default:
+ console.error('Invalid NaCl operation: ' + operation + '.');
+ this.onError_(this.compressorId_);
+ }
+};
diff --git a/unpacker/js/request.js b/unpacker/js/request.js
index 969987f..b0dcb00 100644
--- a/unpacker/js/request.js
+++ b/unpacker/js/request.js
@@ -16,26 +16,42 @@
* @enum {string}
*/
Key: {
- // Mandatory keys for all requests.
+ // Mandatory keys for all unpacking operations.
OPERATION: 'operation', // Should be a unpacker.request.Operation.
FILE_SYSTEM_ID: 'file_system_id', // Should be a string.
REQUEST_ID: 'request_id', // Should be a string.
- // Optional keys depending on request operation.
- ERROR: 'error', // Should be a string.
+ // Optional keys unique to unpacking operations.
METADATA: 'metadata', // Should be a dictionary.
ARCHIVE_SIZE: 'archive_size', // Should be a string as only int is
// supported by pp::Var on C++.
+ INDEX: 'index', // Should be a string. Same reason as ARCHIVE_SIZE.
+ ENCODING: 'encoding', // Should be a string.
+ OPEN_REQUEST_ID: 'open_request_id', // Should be a string, just like
+ // REQUEST_ID.
+ READ_FILE_DATA: 'read_file_data', // Should be an ArrayBuffer.
+ HAS_MORE_DATA: 'has_more_data', // Should be a boolean.
+ PASSPHRASE: 'passphrase', // Should be a string.
+
+ // Mandatory keys for all packing operations.
+ COMPRESSOR_ID: 'compressor_id', // Should be an int.
+
+ // Optional keys unique to packing operations.
+ ENTRY_ID: 'entry_id', // Should be an int.
+ PATHNAME: 'pathname', // should be a string.
+ FILE_SIZE: 'file_size', // should be a string. Same reason
+ // as ARCHIVE_SIZE.
+ IS_DIRECTORY: 'is_directory', // should be a boolean.
+ MODIFICATION_TIME: 'modification_time', // should be a string.
+ // (mm/dd/yy h:m:s)
+ HAS_ERROR: 'has_error', // Should be a boolean Sent from JS
+ // to NaCL.
+
+ // Optional keys used for both packing and unpacking operations.
+ ERROR: 'error', // Should be a string.
CHUNK_BUFFER: 'chunk_buffer', // Should be an ArrayBuffer.
OFFSET: 'offset', // Should be a string. Same reason as ARCHIVE_SIZE.
LENGTH: 'length', // Should be a string. Same reason as ARCHIVE_SIZE.
- INDEX: 'index', // Should be a string. Same reason as ARCHIVE_SIZE.
- ENCODING: 'encoding', // Should be a string.
- OPEN_REQUEST_ID: 'open_request_id', // Should be a string, just like
- // REQUEST_ID.
- READ_FILE_DATA: 'read_file_data', // Should be an ArrayBuffer.
- HAS_MORE_DATA: 'has_more_data', // Should be a boolean.
- PASSPHRASE: 'passphrase', // Should be a string.
SRC_FILE: 'src_file', // Should be a string.
SRC_LINE: 'src_line', // Should be a int.
SRC_FUNC: 'src_func', // Should be a string.
@@ -45,7 +61,9 @@
/**
* Defines request operations. These operation should be the same as the
* operations on the NaCL side. FILE_SYSTEM_ID and REQUEST_ID are mandatory
- * for all requests.
+ * for all unpack requests, while COMPRESSOR_ID is required for all pack
+ * requests. All the values of unpacking operations must be smaller than any
+ * packing operation (except errors).
* @enum {number}
*/
Operation: {
@@ -66,7 +84,34 @@
READ_FILE_DONE: 14,
CONSOLE_LOG: 15,
CONSOLE_DEBUG: 16,
- FILE_SYSTEM_ERROR: -1
+ CREATE_ARCHIVE: 17,
+ CREATE_ARCHIVE_DONE: 18,
+ ADD_TO_ARCHIVE: 19,
+ ADD_TO_ARCHIVE_DONE: 20,
+ READ_FILE_CHUNK: 21,
+ READ_FILE_CHUNK_DONE: 22,
+ WRITE_CHUNK: 23,
+ WRITE_CHUNK_DONE: 24,
+ CLOSE_ARCHIVE: 25,
+ CLOSE_ARCHIVE_DONE: 26,
+ FILE_SYSTEM_ERROR: -1,
+ COMPRESSOR_ERROR: -2
+ },
+
+ /**
+ * Operations greater than or equal to this value are for packing.
+ * @const {number}
+ */
+ MINIMUM_PACK_REQUEST_VALUE: 17,
+
+ /**
+ * Return true if the given operation is related to packing.
+ * @param {!unpacker.request.Operation} operation
+ * @return {boolean}
+ */
+ isPackRequest: function(operation) {
+ return unpacker.request.MINIMUM_PACK_REQUEST_VALUE <= operation ||
+ operation == unpacker.request.Operation.COMPRESSOR_ERROR;
},
/**
@@ -230,5 +275,90 @@
readFileRequest[unpacker.request.Key.OFFSET] = offset.toString();
readFileRequest[unpacker.request.Key.LENGTH] = length.toString();
return readFileRequest;
+ },
+
+ /**
+ * Creates a create archive request for compressor.
+ * @param {!unpacker.types.CompressorId} compressorId
+ * @return {!Object} A create archive request.
+ */
+ createCreateArchiveRequest: function(compressorId) {
+ var request = {};
+ request[unpacker.request.Key.OPERATION] =
+ unpacker.request.Operation.CREATE_ARCHIVE;
+ request[unpacker.request.Key.COMPRESSOR_ID] = compressorId;
+ return request;
+ },
+
+ /**
+ * Creates an add to archive request for compressor.
+ * @param {!unpacker.types.CompressorId} compressorId
+ * @param {!unpacker.types.EntryId} entryId
+ * @param {string} pathname The relative path of the entry.
+ * @param {number} fileSize The size of the entry.
+ * @param {string} modificationTime The modification time of the entry.
+ * @param {boolean} isDirectory Whether the entry is a directory or not.
+ * @return {!Object} An add to archive request.
+ */
+ createAddToArchiveRequest: function(compressorId, entryId, pathname,
+ fileSize, modificationTime, isDirectory) {
+ var request = {};
+ request[unpacker.request.Key.OPERATION] =
+ unpacker.request.Operation.ADD_TO_ARCHIVE;
+ request[unpacker.request.Key.COMPRESSOR_ID] = compressorId;
+ request[unpacker.request.Key.ENTRY_ID] = entryId;
+ request[unpacker.request.Key.PATHNAME] = pathname.toString();
+ request[unpacker.request.Key.FILE_SIZE] = fileSize.toString();
+ request[unpacker.request.Key.MODIFICATION_TIME] =
+ modificationTime.toString();
+ request[unpacker.request.Key.IS_DIRECTORY] = isDirectory;
+ return request;
+ },
+
+ /**
+ * Creates a read file chunk response for compressor.
+ * @param {!unpacker.types.CompressorId} compressorId
+ * @param {number} length The number of bytes read from the entry.
+ * @param {!ArrayBuffer} buffer A buffer containing the data that was read.
+ * @return {!Object} A read file chunk done response.
+ */
+ createReadFileChunkDoneResponse: function(compressorId, length, buffer) {
+ var response = {};
+ response[unpacker.request.Key.OPERATION] =
+ unpacker.request.Operation.READ_FILE_CHUNK_DONE;
+ response[unpacker.request.Key.COMPRESSOR_ID] = compressorId;
+ response[unpacker.request.Key.LENGTH] = length.toString();
+ response[unpacker.request.Key.CHUNK_BUFFER] = buffer;
+ return response;
+ },
+
+ /**
+ * Creates a write chunk done response for compressor.
+ * @param {!unpacker.types.CompressorId} compressorId
+ * @param {number} length The number of bytes written onto the archive file.
+ * @return {!Object} A write chunk done response.
+ */
+ createWriteChunkDoneResponse: function(compressorId, length) {
+ var response = {};
+ response[unpacker.request.Key.OPERATION] =
+ unpacker.request.Operation.WRITE_CHUNK_DONE;
+ response[unpacker.request.Key.COMPRESSOR_ID] = compressorId;
+ response[unpacker.request.Key.LENGTH] = length.toString();
+ return response;
+ },
+
+ /**
+ * Creates a close archive request for compressor.
+ * @param {!unpacker.types.CompressorId} compressorId
+ * @param {boolean} hasError True if some error occurred.
+ * @return {!Object} A close archive request.
+ */
+ createCloseArchiveRequest: function(compressorId, hasError) {
+ var request = {};
+ request[unpacker.request.Key.OPERATION] =
+ unpacker.request.Operation.CLOSE_ARCHIVE;
+ request[unpacker.request.Key.COMPRESSOR_ID] = compressorId;
+ request[unpacker.request.Key.HAS_ERROR] = hasError;
+ return request;
}
};
diff --git a/unpacker/js/types.js b/unpacker/js/types.js
index e899311..360cc0d 100644
--- a/unpacker/js/types.js
+++ b/unpacker/js/types.js
@@ -16,6 +16,12 @@
/** @typedef {number} */
unpacker.types.RequestId;
+/** @typedef {number} */
+unpacker.types.CompressorId;
+
+/** @typedef {number} */
+unpacker.types.EntryId;
+
/**
* @see
* https://developer.chrome.com/apps/fileSystemProvider#event-onUnmountRequested
diff --git a/unpacker/manifest.json b/unpacker/manifest.json
index 1dcb042..b97e392 100644
--- a/unpacker/manifest.json
+++ b/unpacker/manifest.json
@@ -7,20 +7,26 @@
"default_locale": "en",
"display_in_launcher": false,
"permissions": [
+ "alwaysOnTopWindows",
"fileSystemProvider",
- {"fileSystem": ["retainEntries"]},
+ {"fileSystem": ["retainEntries", "write", "directory"]},
"notifications",
- "storage",
- "alwaysOnTopWindows"
+ "storage"
],
"file_system_provider_capabilities": {
"multipleMounts": true,
"source": "file"
},
"file_handlers": {
- "zip": {
- "types": ["application/zip"],
- "extensions": ["zip"]
+ "add": {
+ "types":["application/zip"],
+ "extensions": ["zip"],
+ "verb": "add_to"
+ },
+ "pack": {
+ "types": ["*"],
+ "include_directories": true,
+ "verb": "pack_with"
}
},
"icons": {
@@ -36,6 +42,7 @@
"js/unpacker.js",
"js/app.js",
"js/background.js",
+ "js/compressor.js",
"js/decompressor.js",
"js/passphrase-manager.js",
"js/request.js",