diff options
Diffstat (limited to 'dom/archivereader')
-rw-r--r-- | dom/archivereader/ArchiveEvent.cpp | 131 | ||||
-rw-r--r-- | dom/archivereader/ArchiveEvent.h | 84 | ||||
-rw-r--r-- | dom/archivereader/ArchiveReader.cpp | 217 | ||||
-rw-r--r-- | dom/archivereader/ArchiveReader.h | 119 | ||||
-rw-r--r-- | dom/archivereader/ArchiveReaderCommon.h | 24 | ||||
-rw-r--r-- | dom/archivereader/ArchiveRequest.cpp | 277 | ||||
-rw-r--r-- | dom/archivereader/ArchiveRequest.h | 88 | ||||
-rw-r--r-- | dom/archivereader/ArchiveZipEvent.cpp | 216 | ||||
-rw-r--r-- | dom/archivereader/ArchiveZipEvent.h | 70 | ||||
-rw-r--r-- | dom/archivereader/ArchiveZipFile.cpp | 402 | ||||
-rw-r--r-- | dom/archivereader/ArchiveZipFile.h | 81 | ||||
-rw-r--r-- | dom/archivereader/moz.build | 30 | ||||
-rw-r--r-- | dom/archivereader/test/helpers.js | 31 | ||||
-rw-r--r-- | dom/archivereader/test/mochitest.ini | 7 | ||||
-rw-r--r-- | dom/archivereader/test/test_basic.html | 227 | ||||
-rw-r--r-- | dom/archivereader/test/test_nonUnicode.html | 77 | ||||
-rw-r--r-- | dom/archivereader/test/test_zip_in_zip.html | 111 |
17 files changed, 2192 insertions, 0 deletions
diff --git a/dom/archivereader/ArchiveEvent.cpp b/dom/archivereader/ArchiveEvent.cpp new file mode 100644 index 000000000..b1b3dcd82 --- /dev/null +++ b/dom/archivereader/ArchiveEvent.cpp @@ -0,0 +1,131 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ArchiveEvent.h" + +#include "nsCExternalHandlerService.h" +#include "nsProxyRelease.h" + +USING_ARCHIVEREADER_NAMESPACE + +NS_IMPL_ISUPPORTS0(ArchiveItem) + +ArchiveItem::ArchiveItem() +{ + MOZ_COUNT_CTOR(ArchiveItem); +} + +ArchiveItem::~ArchiveItem() +{ + MOZ_COUNT_DTOR(ArchiveItem); +} + + +nsCString +ArchiveItem::GetType() +{ + if (mType.IsEmpty()) { + return NS_LITERAL_CSTRING("binary/octet-stream"); + } + + return mType; +} + +void +ArchiveItem::SetType(const nsCString& aType) +{ + mType = aType; +} + +ArchiveReaderEvent::ArchiveReaderEvent(ArchiveReader* aArchiveReader) +: mArchiveReader(aArchiveReader) +{ + MOZ_COUNT_CTOR(ArchiveReaderEvent); +} + +ArchiveReaderEvent::~ArchiveReaderEvent() +{ + if (!NS_IsMainThread()) { + NS_ReleaseOnMainThread(mMimeService.forget()); + } + + MOZ_COUNT_DTOR(ArchiveReaderEvent); +} + +// From the filename to the mimetype: +nsresult +ArchiveReaderEvent::GetType(nsCString& aExt, + nsCString& aMimeType) +{ + MOZ_ASSERT(NS_IsMainThread()); + + nsresult rv; + + if (mMimeService.get() == nullptr) { + mMimeService = do_GetService(NS_MIMESERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = mMimeService->GetTypeFromExtension(aExt, aMimeType); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP +ArchiveReaderEvent::Run() +{ + return Exec(); +} + +nsresult +ArchiveReaderEvent::RunShare(nsresult aStatus) +{ + mStatus = aStatus; + + NS_DispatchToMainThread(NewRunnableMethod(this, &ArchiveReaderEvent::ShareMainThread)); + + return NS_OK; +} + +void +ArchiveReaderEvent::ShareMainThread() +{ + nsTArray<RefPtr<File>> fileList; + + if (!NS_FAILED(mStatus)) { + // This extra step must run in the main thread: + for (uint32_t index = 0; index < mFileList.Length(); ++index) { + RefPtr<ArchiveItem> item = mFileList[index]; + + nsString tmp; + nsresult rv = item->GetFilename(tmp); + nsCString filename = NS_ConvertUTF16toUTF8(tmp); + if (NS_FAILED(rv)) { + continue; + } + + int32_t offset = filename.RFindChar('.'); + if (offset != kNotFound) { + filename.Cut(0, offset + 1); + + // Just to be sure, if something goes wrong, the mimetype is an empty string: + nsCString type; + if (NS_SUCCEEDED(GetType(filename, type))) { + item->SetType(type); + } + } + + // This is a File: + RefPtr<File> file = item->GetFile(mArchiveReader); + if (file) { + fileList.AppendElement(file); + } + } + } + + mArchiveReader->Ready(fileList, mStatus); +} diff --git a/dom/archivereader/ArchiveEvent.h b/dom/archivereader/ArchiveEvent.h new file mode 100644 index 000000000..92ac2774d --- /dev/null +++ b/dom/archivereader/ArchiveEvent.h @@ -0,0 +1,84 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_archivereader_domarchiveevent_h__ +#define mozilla_dom_archivereader_domarchiveevent_h__ + +#include "ArchiveReader.h" + +#include "mozilla/dom/File.h" +#include "nsISeekableStream.h" +#include "nsIMIMEService.h" + +#include "ArchiveReaderCommon.h" + +BEGIN_ARCHIVEREADER_NAMESPACE + +/** + * This class contains all the info needed for a single item + * It must contain the implementation of the File() method. + */ +class ArchiveItem : public nsISupports +{ +public: + NS_DECL_THREADSAFE_ISUPPORTS + + ArchiveItem(); + + // Getter/Setter for the type + nsCString GetType(); + void SetType(const nsCString& aType); + + // Getter for the filename + virtual nsresult GetFilename(nsString& aFilename) = 0; + + // Generate a File + virtual already_AddRefed<File> GetFile(ArchiveReader* aArchiveReader) = 0; + +protected: + virtual ~ArchiveItem(); + + nsCString mType; +}; + +/** + * This class must be extended by any archive format supported by ArchiveReader API + * This class runs in a different thread and it calls the 'exec()' method. + * The exec() must populate mFileList and mStatus then it must call RunShare(); + */ +class ArchiveReaderEvent : public Runnable +{ +public: + NS_DECL_NSIRUNNABLE + + explicit ArchiveReaderEvent(ArchiveReader* aArchiveReader); + +protected: + virtual ~ArchiveReaderEvent(); + +public: + // This must be implemented + virtual nsresult Exec() = 0; + +protected: + nsresult GetType(nsCString& aExt, + nsCString& aMimeType); + + nsresult RunShare(nsresult aStatus); + void ShareMainThread(); + +protected: // data + ArchiveReader* mArchiveReader; + + nsCOMPtr<nsIMIMEService> mMimeService; + + nsTArray<RefPtr<ArchiveItem> > mFileList; // this must be populated + nsresult mStatus; +}; + +END_ARCHIVEREADER_NAMESPACE + +#endif // mozilla_dom_archivereader_domarchiveevent_h__ diff --git a/dom/archivereader/ArchiveReader.cpp b/dom/archivereader/ArchiveReader.cpp new file mode 100644 index 000000000..f6985d989 --- /dev/null +++ b/dom/archivereader/ArchiveReader.cpp @@ -0,0 +1,217 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ArchiveReader.h" +#include "ArchiveRequest.h" +#include "ArchiveEvent.h" +#include "ArchiveZipEvent.h" + +#include "nsIURI.h" +#include "nsNetCID.h" + +#include "mozilla/dom/ArchiveReaderBinding.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/EncodingUtils.h" +#include "mozilla/Preferences.h" + +using namespace mozilla; +using namespace mozilla::dom; +USING_ARCHIVEREADER_NAMESPACE + +/* static */ already_AddRefed<ArchiveReader> +ArchiveReader::Constructor(const GlobalObject& aGlobal, + Blob& aBlob, + const ArchiveReaderOptions& aOptions, + ErrorResult& aError) +{ + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(aGlobal.GetAsSupports()); + if (!window) { + aError.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + + nsAutoCString encoding; + if (!EncodingUtils::FindEncodingForLabelNoReplacement(aOptions.mEncoding, + encoding)) { + aError.ThrowRangeError<MSG_ENCODING_NOT_SUPPORTED>(aOptions.mEncoding); + return nullptr; + } + + RefPtr<ArchiveReader> reader = + new ArchiveReader(aBlob, window, encoding); + return reader.forget(); +} + +ArchiveReader::ArchiveReader(Blob& aBlob, nsPIDOMWindowInner* aWindow, + const nsACString& aEncoding) + : mBlobImpl(aBlob.Impl()) + , mWindow(aWindow) + , mStatus(NOT_STARTED) + , mEncoding(aEncoding) +{ + MOZ_ASSERT(aWindow); +} + +ArchiveReader::~ArchiveReader() +{ +} + +/* virtual */ JSObject* +ArchiveReader::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return ArchiveReaderBinding::Wrap(aCx, this, aGivenProto); +} + +nsresult +ArchiveReader::RegisterRequest(ArchiveRequest* aRequest) +{ + switch (mStatus) { + // Append to the list and let's start to work: + case NOT_STARTED: + mRequests.AppendElement(aRequest); + return OpenArchive(); + + // Just append to the list: + case WORKING: + mRequests.AppendElement(aRequest); + return NS_OK; + + // Return data! + case READY: + RequestReady(aRequest); + return NS_OK; + } + + NS_ASSERTION(false, "unexpected mStatus value"); + return NS_OK; +} + +// This returns the input stream +nsresult +ArchiveReader::GetInputStream(nsIInputStream** aInputStream) +{ + // Getting the input stream + ErrorResult rv; + mBlobImpl->GetInternalStream(aInputStream, rv); + if (NS_WARN_IF(rv.Failed())) { + return rv.StealNSResult(); + } + + return NS_OK; +} + +nsresult +ArchiveReader::GetSize(uint64_t* aSize) +{ + ErrorResult rv; + *aSize = mBlobImpl->GetSize(rv); + return rv.StealNSResult(); +} + +// Here we open the archive: +nsresult +ArchiveReader::OpenArchive() +{ + mStatus = WORKING; + nsresult rv; + + // Target: + nsCOMPtr<nsIEventTarget> target = do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); + NS_ASSERTION(target, "Must have stream transport service"); + + // Here a Event to make everything async: + RefPtr<ArchiveReaderEvent> event; + + /* FIXME: If we want to support more than 1 format we should check the content type here: */ + event = new ArchiveReaderZipEvent(this, mEncoding); + rv = target->Dispatch(event, NS_DISPATCH_NORMAL); + NS_ENSURE_SUCCESS(rv, rv); + + // In order to be sure that this object exists when the event finishes its task, + // we increase the refcount here: + AddRef(); + + return NS_OK; +} + +// Data received from the dispatched event: +void +ArchiveReader::Ready(nsTArray<RefPtr<File>>& aFileList, + nsresult aStatus) +{ + mStatus = READY; + + // Let's store the values: + mData.fileList = aFileList; + mData.status = aStatus; + + // Propagate the results: + for (uint32_t index = 0; index < mRequests.Length(); ++index) { + RefPtr<ArchiveRequest> request = mRequests[index]; + RequestReady(request); + } + + mRequests.Clear(); + + // The async operation is concluded, we can decrease the reference: + Release(); +} + +void +ArchiveReader::RequestReady(ArchiveRequest* aRequest) +{ + // The request will do the rest: + aRequest->ReaderReady(mData.fileList, mData.status); +} + +already_AddRefed<ArchiveRequest> +ArchiveReader::GetFilenames() +{ + RefPtr<ArchiveRequest> request = GenerateArchiveRequest(); + request->OpGetFilenames(); + + return request.forget(); +} + +already_AddRefed<ArchiveRequest> +ArchiveReader::GetFile(const nsAString& filename) +{ + RefPtr<ArchiveRequest> request = GenerateArchiveRequest(); + request->OpGetFile(filename); + + return request.forget(); +} + +already_AddRefed<ArchiveRequest> +ArchiveReader::GetFiles() +{ + RefPtr<ArchiveRequest> request = GenerateArchiveRequest(); + request->OpGetFiles(); + + return request.forget(); +} + +already_AddRefed<ArchiveRequest> +ArchiveReader::GenerateArchiveRequest() +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + return ArchiveRequest::Create(mWindow, this); +} + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ArchiveReader, + mBlobImpl, + mWindow, + mData.fileList, + mRequests) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ArchiveReader) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(ArchiveReader) +NS_IMPL_CYCLE_COLLECTING_RELEASE(ArchiveReader) diff --git a/dom/archivereader/ArchiveReader.h b/dom/archivereader/ArchiveReader.h new file mode 100644 index 000000000..687392e56 --- /dev/null +++ b/dom/archivereader/ArchiveReader.h @@ -0,0 +1,119 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_archivereader_domarchivereader_h__ +#define mozilla_dom_archivereader_domarchivereader_h__ + +#include "nsWrapperCache.h" + +#include "ArchiveReaderCommon.h" + +#include "nsCOMArray.h" +#include "nsIChannel.h" +#include "mozilla/Attributes.h" + +namespace mozilla { +namespace dom { +struct ArchiveReaderOptions; +class Blob; +class BlobImpl; +class File; +class GlobalObject; +} // namespace dom +} // namespace mozilla + +BEGIN_ARCHIVEREADER_NAMESPACE + +class ArchiveRequest; + +/** + * This is the ArchiveReader object + */ +class ArchiveReader final : public nsISupports, + public nsWrapperCache +{ +public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(ArchiveReader) + + static already_AddRefed<ArchiveReader> + Constructor(const GlobalObject& aGlobal, Blob& aBlob, + const ArchiveReaderOptions& aOptions, ErrorResult& aError); + + ArchiveReader(Blob& aBlob, nsPIDOMWindowInner* aWindow, + const nsACString& aEncoding); + + nsPIDOMWindowInner* GetParentObject() const + { + return mWindow; + } + + virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + already_AddRefed<ArchiveRequest> GetFilenames(); + already_AddRefed<ArchiveRequest> GetFile(const nsAString& filename); + already_AddRefed<ArchiveRequest> GetFiles(); + + nsresult GetInputStream(nsIInputStream** aInputStream); + nsresult GetSize(uint64_t* aSize); + +public: // for the ArchiveRequest: + nsresult RegisterRequest(ArchiveRequest* aRequest); + +public: // For events: + BlobImpl* GetBlobImpl() const + { + return mBlobImpl; + } + + void Ready(nsTArray<RefPtr<File>>& aFileList, nsresult aStatus); + +private: + ~ArchiveReader(); + + already_AddRefed<ArchiveRequest> GenerateArchiveRequest(); + + nsresult OpenArchive(); + + void RequestReady(ArchiveRequest* aRequest); + +protected: + // The archive blob/file + RefPtr<BlobImpl> mBlobImpl; + + // The window is needed by the requests + nsCOMPtr<nsPIDOMWindowInner> mWindow; + + // Are we ready to return data? + enum { + NOT_STARTED = 0, + WORKING, + READY + } mStatus; + + // State of the read: + enum { + Header, // We are collecting the header: 30bytes + Name, // The name length is contained in the header + Data, // The length of the data segment COULD be written in the header + Search // ... if the data length is unknown (== 0) we wait until we read a new header + } mReadStatus; + + // List of requests to be processed + nsTArray<RefPtr<ArchiveRequest> > mRequests; + + // Everything related to the blobs and the status: + struct { + nsTArray<RefPtr<File>> fileList; + nsresult status; + } mData; + + nsCString mEncoding; +}; + +END_ARCHIVEREADER_NAMESPACE + +#endif // mozilla_dom_archivereader_domarchivereader_h__ diff --git a/dom/archivereader/ArchiveReaderCommon.h b/dom/archivereader/ArchiveReaderCommon.h new file mode 100644 index 000000000..ab40c3bef --- /dev/null +++ b/dom/archivereader/ArchiveReaderCommon.h @@ -0,0 +1,24 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_archivereader_archivereader_h +#define mozilla_dom_archivereader_archivereader_h + +#include "mozilla/DOMEventTargetHelper.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsDebug.h" +#include "nsString.h" +#include "nsTArray.h" + +#define BEGIN_ARCHIVEREADER_NAMESPACE \ + namespace mozilla { namespace dom { namespace archivereader { +#define END_ARCHIVEREADER_NAMESPACE \ + } /* namespace archivereader */ } /* namespace dom */ } /* namespace mozilla */ +#define USING_ARCHIVEREADER_NAMESPACE \ + using namespace mozilla::dom::archivereader; + +#endif // mozilla_dom_archivereader_archivereadercommon_h diff --git a/dom/archivereader/ArchiveRequest.cpp b/dom/archivereader/ArchiveRequest.cpp new file mode 100644 index 000000000..ec1686804 --- /dev/null +++ b/dom/archivereader/ArchiveRequest.cpp @@ -0,0 +1,277 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ArchiveRequest.h" + +#include "mozilla/EventDispatcher.h" +#include "mozilla/dom/ArchiveRequestBinding.h" +#include "mozilla/dom/ScriptSettings.h" +#include "nsContentUtils.h" + +using namespace mozilla; + +USING_ARCHIVEREADER_NAMESPACE + +/** + * Class used to make asynchronous the ArchiveRequest. + */ +class ArchiveRequestEvent : public Runnable +{ +public: + NS_DECL_NSIRUNNABLE + + explicit ArchiveRequestEvent(ArchiveRequest* aRequest) + : mRequest(aRequest) + { + MOZ_COUNT_CTOR(ArchiveRequestEvent); + } + +protected: + ~ArchiveRequestEvent() + { + MOZ_COUNT_DTOR(ArchiveRequestEvent); + } + +private: //data + RefPtr<ArchiveRequest> mRequest; +}; + +NS_IMETHODIMP +ArchiveRequestEvent::Run() +{ + MOZ_ASSERT(mRequest, "the request is not longer valid"); + mRequest->Run(); + return NS_OK; +} + +// ArchiveRequest + +ArchiveRequest::ArchiveRequest(nsPIDOMWindowInner* aWindow, + ArchiveReader* aReader) +: DOMRequest(aWindow), + mArchiveReader(aReader) +{ + MOZ_ASSERT(aReader); + + MOZ_COUNT_CTOR(ArchiveRequest); + + /* An event to make this request asynchronous: */ + RefPtr<ArchiveRequestEvent> event = new ArchiveRequestEvent(this); + NS_DispatchToCurrentThread(event); +} + +ArchiveRequest::~ArchiveRequest() +{ + MOZ_COUNT_DTOR(ArchiveRequest); +} + +nsresult +ArchiveRequest::PreHandleEvent(EventChainPreVisitor& aVisitor) +{ + aVisitor.mCanHandle = true; + aVisitor.mParentTarget = nullptr; + return NS_OK; +} + +/* virtual */ JSObject* +ArchiveRequest::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return ArchiveRequestBinding::Wrap(aCx, this, aGivenProto); +} + +ArchiveReader* +ArchiveRequest::Reader() const +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + return mArchiveReader; +} + +// Here the request is processed: +void +ArchiveRequest::Run() +{ + // Register this request to the reader. + // When the reader is ready to return data, a 'Ready()' will be called + nsresult rv = mArchiveReader->RegisterRequest(this); + if (NS_FAILED(rv)) { + FireError(rv); + } +} + +void +ArchiveRequest::OpGetFilenames() +{ + mOperation = GetFilenames; +} + +void +ArchiveRequest::OpGetFile(const nsAString& aFilename) +{ + mOperation = GetFile; + mFilename = aFilename; +} + +void +ArchiveRequest::OpGetFiles() +{ + mOperation = GetFiles; +} + +nsresult +ArchiveRequest::ReaderReady(nsTArray<RefPtr<File>>& aFileList, + nsresult aStatus) +{ + if (NS_FAILED(aStatus)) { + FireError(aStatus); + return NS_OK; + } + + nsresult rv; + + AutoJSAPI jsapi; + if (NS_WARN_IF(!jsapi.Init(GetOwner()))) { + return NS_ERROR_UNEXPECTED; + } + JSContext* cx = jsapi.cx(); + + JS::Rooted<JS::Value> result(cx); + switch (mOperation) { + case GetFilenames: + rv = GetFilenamesResult(cx, result.address(), aFileList); + break; + + case GetFile: + rv = GetFileResult(cx, &result, aFileList); + break; + + case GetFiles: + rv = GetFilesResult(cx, &result, aFileList); + break; + + default: + rv = NS_ERROR_UNEXPECTED; + break; + } + + if (NS_FAILED(rv)) { + NS_WARNING("Get*Result failed!"); + } + + if (NS_SUCCEEDED(rv)) { + FireSuccess(result); + } + else { + FireError(rv); + } + + return NS_OK; +} + +nsresult +ArchiveRequest::GetFilenamesResult(JSContext* aCx, + JS::Value* aValue, + nsTArray<RefPtr<File>>& aFileList) +{ + JS::Rooted<JSObject*> array(aCx, JS_NewArrayObject(aCx, aFileList.Length())); + + if (!array) { + return NS_ERROR_OUT_OF_MEMORY; + } + + JS::Rooted<JSString*> str(aCx); + for (uint32_t i = 0; i < aFileList.Length(); ++i) { + RefPtr<File> file = aFileList[i]; + + nsString filename; + file->GetName(filename); + + str = JS_NewUCStringCopyZ(aCx, filename.get()); + NS_ENSURE_TRUE(str, NS_ERROR_OUT_OF_MEMORY); + + if (!JS_DefineElement(aCx, array, i, str, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + } + + if (!JS_FreezeObject(aCx, array)) { + return NS_ERROR_FAILURE; + } + + aValue->setObject(*array); + return NS_OK; +} + +nsresult +ArchiveRequest::GetFileResult(JSContext* aCx, + JS::MutableHandle<JS::Value> aValue, + nsTArray<RefPtr<File>>& aFileList) +{ + for (uint32_t i = 0; i < aFileList.Length(); ++i) { + RefPtr<File> file = aFileList[i]; + + nsString filename; + file->GetName(filename); + + if (filename == mFilename) { + if (!ToJSValue(aCx, file, aValue)) { + return NS_ERROR_FAILURE; + } + + return NS_OK; + } + } + + return NS_ERROR_FAILURE; +} + +nsresult +ArchiveRequest::GetFilesResult(JSContext* aCx, + JS::MutableHandle<JS::Value> aValue, + nsTArray<RefPtr<File>>& aFileList) +{ + JS::Rooted<JSObject*> array(aCx, JS_NewArrayObject(aCx, aFileList.Length())); + if (!array) { + return NS_ERROR_OUT_OF_MEMORY; + } + + for (uint32_t i = 0; i < aFileList.Length(); ++i) { + RefPtr<File> file = aFileList[i]; + + JS::Rooted<JS::Value> value(aCx); + if (!ToJSValue(aCx, file, &value)) { + return NS_ERROR_FAILURE; + } + + if (!JS_DefineElement(aCx, array, i, value, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + } + + aValue.setObject(*array); + return NS_OK; +} + +// static +already_AddRefed<ArchiveRequest> +ArchiveRequest::Create(nsPIDOMWindowInner* aOwner, + ArchiveReader* aReader) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + RefPtr<ArchiveRequest> request = new ArchiveRequest(aOwner, aReader); + + return request.forget(); +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(ArchiveRequest, DOMRequest, + mArchiveReader) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(ArchiveRequest) +NS_INTERFACE_MAP_END_INHERITING(DOMRequest) + +NS_IMPL_ADDREF_INHERITED(ArchiveRequest, DOMRequest) +NS_IMPL_RELEASE_INHERITED(ArchiveRequest, DOMRequest) diff --git a/dom/archivereader/ArchiveRequest.h b/dom/archivereader/ArchiveRequest.h new file mode 100644 index 000000000..3988f1aa1 --- /dev/null +++ b/dom/archivereader/ArchiveRequest.h @@ -0,0 +1,88 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_archivereader_domarchiverequest_h__ +#define mozilla_dom_archivereader_domarchiverequest_h__ + +#include "mozilla/Attributes.h" +#include "ArchiveReader.h" +#include "DOMRequest.h" + +#include "ArchiveReaderCommon.h" + +namespace mozilla { +class EventChainPreVisitor; +} // namespace mozilla + +BEGIN_ARCHIVEREADER_NAMESPACE + +/** + * This is the ArchiveRequest that handles any operation + * related to ArchiveReader + */ +class ArchiveRequest : public mozilla::dom::DOMRequest +{ +public: + virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + ArchiveReader* Reader() const; + + NS_DECL_ISUPPORTS_INHERITED + + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ArchiveRequest, DOMRequest) + + ArchiveRequest(nsPIDOMWindowInner* aWindow, + ArchiveReader* aReader); + + // nsIDOMEventTarget + virtual nsresult PreHandleEvent(EventChainPreVisitor& aVisitor) override; + +public: + // This is called by the DOMArchiveRequestEvent + void Run(); + + // Set the types for this request + void OpGetFilenames(); + void OpGetFile(const nsAString& aFilename); + void OpGetFiles(); + + nsresult ReaderReady(nsTArray<RefPtr<File>>& aFileList, nsresult aStatus); + +public: // static + static already_AddRefed<ArchiveRequest> Create(nsPIDOMWindowInner* aOwner, + ArchiveReader* aReader); + +private: + ~ArchiveRequest(); + + nsresult GetFilenamesResult(JSContext* aCx, + JS::Value* aValue, + nsTArray<RefPtr<File>>& aFileList); + nsresult GetFileResult(JSContext* aCx, + JS::MutableHandle<JS::Value> aValue, + nsTArray<RefPtr<File>>& aFileList); + nsresult GetFilesResult(JSContext* aCx, + JS::MutableHandle<JS::Value> aValue, + nsTArray<RefPtr<File>>& aFileList); + +protected: + // The reader: + RefPtr<ArchiveReader> mArchiveReader; + + // The operation: + enum { + GetFilenames, + GetFile, + GetFiles + } mOperation; + + // The filename (needed by GetFile): + nsString mFilename; +}; + +END_ARCHIVEREADER_NAMESPACE + +#endif // mozilla_dom_archivereader_domarchiverequest_h__ diff --git a/dom/archivereader/ArchiveZipEvent.cpp b/dom/archivereader/ArchiveZipEvent.cpp new file mode 100644 index 000000000..56251eef6 --- /dev/null +++ b/dom/archivereader/ArchiveZipEvent.cpp @@ -0,0 +1,216 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ArchiveZipEvent.h" +#include "ArchiveZipFile.h" + +#include "nsContentUtils.h" +#include "nsCExternalHandlerService.h" + +#include "mozilla/UniquePtr.h" + +using namespace mozilla; +using namespace mozilla::dom; + +USING_ARCHIVEREADER_NAMESPACE + +#ifndef PATH_MAX +# define PATH_MAX 65536 // The filename length is stored in 2 bytes +#endif + +ArchiveZipItem::ArchiveZipItem(const char* aFilename, + const ZipCentral& aCentralStruct, + const nsACString& aEncoding) +: mFilename(aFilename), + mCentralStruct(aCentralStruct), + mEncoding(aEncoding) +{ + MOZ_COUNT_CTOR(ArchiveZipItem); +} + +ArchiveZipItem::~ArchiveZipItem() +{ + MOZ_COUNT_DTOR(ArchiveZipItem); +} + +nsresult +ArchiveZipItem::ConvertFilename() +{ + if (mEncoding.IsEmpty()) { + return NS_ERROR_FAILURE; + } + + nsString filenameU; + nsresult rv = nsContentUtils::ConvertStringFromEncoding( + mEncoding, + mFilename, filenameU); + NS_ENSURE_SUCCESS(rv, rv); + + if (filenameU.IsEmpty()) { + return NS_ERROR_FAILURE; + } + + mFilenameU = filenameU; + return NS_OK; +} + +nsresult +ArchiveZipItem::GetFilename(nsString& aFilename) +{ + if (mFilenameU.IsEmpty()) { + // Maybe this string is UTF-8: + if (IsUTF8(mFilename, false)) { + mFilenameU = NS_ConvertUTF8toUTF16(mFilename); + } + + // Let's use the enconding value for the dictionary + else { + nsresult rv = ConvertFilename(); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + aFilename = mFilenameU; + return NS_OK; +} + +// From zipItem to File: +already_AddRefed<File> +ArchiveZipItem::GetFile(ArchiveReader* aArchiveReader) +{ + nsString filename; + + if (NS_FAILED(GetFilename(filename))) { + return nullptr; + } + + RefPtr<dom::File> file = dom::File::Create(aArchiveReader, + new ArchiveZipBlobImpl(filename, + NS_ConvertUTF8toUTF16(GetType()), + StrToInt32(mCentralStruct.orglen), + mCentralStruct, aArchiveReader->GetBlobImpl())); + MOZ_ASSERT(file); + return file.forget(); +} + +uint32_t +ArchiveZipItem::StrToInt32(const uint8_t* aStr) +{ + return (uint32_t)( (aStr [0] << 0) | + (aStr [1] << 8) | + (aStr [2] << 16) | + (aStr [3] << 24) ); +} + +uint16_t +ArchiveZipItem::StrToInt16(const uint8_t* aStr) +{ + return (uint16_t) ((aStr [0]) | (aStr [1] << 8)); +} + +// ArchiveReaderZipEvent + +ArchiveReaderZipEvent::ArchiveReaderZipEvent(ArchiveReader* aArchiveReader, + const nsACString& aEncoding) +: ArchiveReaderEvent(aArchiveReader), + mEncoding(aEncoding) +{ +} + +// NOTE: this runs in a different thread!! +nsresult +ArchiveReaderZipEvent::Exec() +{ + uint32_t centralOffset(0); + nsresult rv; + + nsCOMPtr<nsIInputStream> inputStream; + rv = mArchiveReader->GetInputStream(getter_AddRefs(inputStream)); + if (NS_FAILED(rv) || !inputStream) { + return RunShare(NS_ERROR_UNEXPECTED); + } + + // From the input stream to a seekable stream + nsCOMPtr<nsISeekableStream> seekableStream; + seekableStream = do_QueryInterface(inputStream); + if (!seekableStream) { + return RunShare(NS_ERROR_UNEXPECTED); + } + + uint64_t size; + rv = mArchiveReader->GetSize(&size); + if (NS_FAILED(rv)) { + return RunShare(NS_ERROR_UNEXPECTED); + } + + // Reading backward.. looking for the ZipEnd signature + for (uint64_t curr = size - ZIPEND_SIZE; curr > 4; --curr) { + seekableStream->Seek(nsISeekableStream::NS_SEEK_SET, curr); + + uint8_t buffer[ZIPEND_SIZE]; + uint32_t ret; + + rv = inputStream->Read((char*)buffer, sizeof(buffer), &ret); + if (NS_FAILED(rv) || ret != sizeof(buffer)) { + return RunShare(NS_ERROR_UNEXPECTED); + } + + // Here we are: + if (ArchiveZipItem::StrToInt32(buffer) == ENDSIG) { + centralOffset = ArchiveZipItem::StrToInt32(((ZipEnd*)buffer)->offset_central_dir); + break; + } + } + + // No central Offset + if (!centralOffset || centralOffset >= size - ZIPEND_SIZE) { + return RunShare(NS_ERROR_FAILURE); + } + + // Seek to the first central directory: + seekableStream->Seek(nsISeekableStream::NS_SEEK_SET, centralOffset); + + // For each central directory: + while (centralOffset <= size - ZIPCENTRAL_SIZE) { + ZipCentral centralStruct; + uint32_t ret; + + rv = inputStream->Read((char*)¢ralStruct, ZIPCENTRAL_SIZE, &ret); + if (NS_FAILED(rv) || ret != ZIPCENTRAL_SIZE) { + return RunShare(NS_ERROR_UNEXPECTED); + } + + uint16_t filenameLen = ArchiveZipItem::StrToInt16(centralStruct.filename_len); + uint16_t extraLen = ArchiveZipItem::StrToInt16(centralStruct.extrafield_len); + uint16_t commentLen = ArchiveZipItem::StrToInt16(centralStruct.commentfield_len); + + // Point to the next item at the top of loop + centralOffset += ZIPCENTRAL_SIZE + filenameLen + extraLen + commentLen; + if (filenameLen == 0 || filenameLen >= PATH_MAX || centralOffset >= size) { + return RunShare(NS_ERROR_FILE_CORRUPTED); + } + + // Read the name: + auto filename = MakeUnique<char[]>(filenameLen + 1); + rv = inputStream->Read(filename.get(), filenameLen, &ret); + if (NS_FAILED(rv) || ret != filenameLen) { + return RunShare(NS_ERROR_UNEXPECTED); + } + + filename[filenameLen] = 0; + + // We ignore the directories: + if (filename[filenameLen - 1] != '/') { + mFileList.AppendElement(new ArchiveZipItem(filename.get(), centralStruct, + mEncoding)); + } + + // Ignore the rest + seekableStream->Seek(nsISeekableStream::NS_SEEK_CUR, extraLen + commentLen); + } + + return RunShare(NS_OK); +} diff --git a/dom/archivereader/ArchiveZipEvent.h b/dom/archivereader/ArchiveZipEvent.h new file mode 100644 index 000000000..2c25740f3 --- /dev/null +++ b/dom/archivereader/ArchiveZipEvent.h @@ -0,0 +1,70 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_archivereader_domarchivezipevent_h__ +#define mozilla_dom_archivereader_domarchivezipevent_h__ + +#include "mozilla/Attributes.h" +#include "ArchiveEvent.h" + +#include "ArchiveReaderCommon.h" +#include "zipstruct.h" + +BEGIN_ARCHIVEREADER_NAMESPACE + +/** + * ArchiveZipItem - ArchiveItem for ArchiveReaderZipEvent + */ +class ArchiveZipItem : public ArchiveItem +{ +public: + ArchiveZipItem(const char* aFilename, + const ZipCentral& aCentralStruct, + const nsACString& aEncoding); +protected: + virtual ~ArchiveZipItem(); + +public: + nsresult GetFilename(nsString& aFilename) override; + + // From zipItem to File: + virtual already_AddRefed<File> + GetFile(ArchiveReader* aArchiveReader) override; + +public: // for the event + static uint32_t StrToInt32(const uint8_t* aStr); + static uint16_t StrToInt16(const uint8_t* aStr); + +private: + nsresult ConvertFilename(); + +private: // data + nsCString mFilename; + + nsString mFilenameU; + ZipCentral mCentralStruct; + + nsCString mEncoding; +}; + +/** + * ArchiveReaderEvent implements the ArchiveReaderEvent for the ZIP format + */ +class ArchiveReaderZipEvent : public ArchiveReaderEvent +{ +public: + ArchiveReaderZipEvent(ArchiveReader* aArchiveReader, + const nsACString& aEncoding); + + nsresult Exec() override; + +private: + nsCString mEncoding; +}; + +END_ARCHIVEREADER_NAMESPACE + +#endif // mozilla_dom_archivereader_domarchivezipevent_h__ diff --git a/dom/archivereader/ArchiveZipFile.cpp b/dom/archivereader/ArchiveZipFile.cpp new file mode 100644 index 000000000..d374fe91f --- /dev/null +++ b/dom/archivereader/ArchiveZipFile.cpp @@ -0,0 +1,402 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ArchiveZipFile.h" +#include "ArchiveZipEvent.h" + +#include "nsIInputStream.h" +#include "zlib.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/File.h" + +using namespace mozilla::dom; +USING_ARCHIVEREADER_NAMESPACE + +#define ZIP_CHUNK 16384 + +/** + * Input stream object for zip files + */ +class ArchiveInputStream final : public nsIInputStream, + public nsISeekableStream +{ +public: + ArchiveInputStream(uint64_t aParentSize, + nsIInputStream* aInputStream, + nsString& aFilename, + uint32_t aStart, + uint32_t aLength, + ZipCentral& aCentral) + : mCentral(aCentral), + mFilename(aFilename), + mStart(aStart), + mLength(aLength), + mStatus(NotStarted) + { + MOZ_COUNT_CTOR(ArchiveInputStream); + + // Reset the data: + memset(&mData, 0, sizeof(mData)); + + mData.parentSize = aParentSize; + mData.inputStream = aInputStream; + } + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIINPUTSTREAM + NS_DECL_NSISEEKABLESTREAM + +private: + virtual ~ArchiveInputStream() + { + MOZ_COUNT_DTOR(ArchiveInputStream); + Close(); + } + + nsresult Init(); + +private: // data + ZipCentral mCentral; + nsString mFilename; + uint32_t mStart; + uint32_t mLength; + + z_stream mZs; + + enum { + NotStarted, + Started, + Done + } mStatus; + + struct { + uint64_t parentSize; + nsCOMPtr<nsIInputStream> inputStream; + + unsigned char input[ZIP_CHUNK]; + uint32_t sizeToBeRead; + uint32_t cursor; + + bool compressed; // a zip file can contain stored or compressed files + } mData; +}; + +NS_IMPL_ISUPPORTS(ArchiveInputStream, + nsIInputStream, + nsISeekableStream) + +nsresult +ArchiveInputStream::Init() +{ + nsresult rv; + + memset(&mZs, 0, sizeof(z_stream)); + int zerr = inflateInit2(&mZs, -MAX_WBITS); + if (zerr != Z_OK) { + return NS_ERROR_OUT_OF_MEMORY; + } + + mData.sizeToBeRead = ArchiveZipItem::StrToInt32(mCentral.size); + + uint32_t offset = ArchiveZipItem::StrToInt32(mCentral.localhdr_offset); + + // The file is corrupt + if (mData.parentSize < ZIPLOCAL_SIZE || + offset > mData.parentSize - ZIPLOCAL_SIZE) { + return NS_ERROR_UNEXPECTED; + } + + // From the input stream to a seekable stream + nsCOMPtr<nsISeekableStream> seekableStream; + seekableStream = do_QueryInterface(mData.inputStream); + if (!seekableStream) { + return NS_ERROR_UNEXPECTED; + } + + // Seek + read the ZipLocal struct + seekableStream->Seek(nsISeekableStream::NS_SEEK_SET, offset); + uint8_t buffer[ZIPLOCAL_SIZE]; + uint32_t ret; + + rv = mData.inputStream->Read((char*)buffer, ZIPLOCAL_SIZE, &ret); + if (NS_FAILED(rv) || ret != ZIPLOCAL_SIZE) { + return NS_ERROR_UNEXPECTED; + } + + // Signature check: + if (ArchiveZipItem::StrToInt32(buffer) != LOCALSIG) { + return NS_ERROR_UNEXPECTED; + } + + ZipLocal local; + memcpy(&local, buffer, ZIPLOCAL_SIZE); + + // Seek to the real data: + offset += ZIPLOCAL_SIZE + + ArchiveZipItem::StrToInt16(local.filename_len) + + ArchiveZipItem::StrToInt16(local.extrafield_len); + + // The file is corrupt if there is not enough data + if (mData.parentSize < mData.sizeToBeRead || + offset > mData.parentSize - mData.sizeToBeRead) { + return NS_ERROR_UNEXPECTED; + } + + // Data starts here: + seekableStream->Seek(nsISeekableStream::NS_SEEK_SET, offset); + + // The file is compressed or not? + mData.compressed = (ArchiveZipItem::StrToInt16(mCentral.method) != 0); + + // We have to skip the first mStart bytes: + if (mStart != 0) { + rv = Seek(NS_SEEK_SET, mStart); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +NS_IMETHODIMP +ArchiveInputStream::Close() +{ + if (mStatus != NotStarted) { + inflateEnd(&mZs); + mStatus = NotStarted; + } + + return NS_OK; +} + +NS_IMETHODIMP +ArchiveInputStream::Available(uint64_t* _retval) +{ + *_retval = mLength - mData.cursor - mStart; + return NS_OK; +} + +NS_IMETHODIMP +ArchiveInputStream::Read(char* aBuffer, + uint32_t aCount, + uint32_t* _retval) +{ + NS_ENSURE_ARG_POINTER(aBuffer); + NS_ENSURE_ARG_POINTER(_retval); + + nsresult rv; + + // This is the first time: + if (mStatus == NotStarted) { + mStatus = Started; + + rv = Init(); + if (NS_FAILED(rv)) { + return rv; + } + + // Let's set avail_out to -1 so we read something from the stream. + mZs.avail_out = (uInt)-1; + } + + // Nothing more can be read + if (mStatus == Done) { + *_retval = 0; + return NS_OK; + } + + // Stored file: + if (!mData.compressed) { + rv = mData.inputStream->Read(aBuffer, + (mData.sizeToBeRead > aCount ? + aCount : mData.sizeToBeRead), + _retval); + if (NS_SUCCEEDED(rv)) { + mData.sizeToBeRead -= *_retval; + mData.cursor += *_retval; + + if (mData.sizeToBeRead == 0) { + mStatus = Done; + } + } + + return rv; + } + + // We have nothing ready to be processed: + if (mZs.avail_out != 0 && mData.sizeToBeRead != 0) { + uint32_t ret; + rv = mData.inputStream->Read((char*)mData.input, + (mData.sizeToBeRead > sizeof(mData.input) ? + sizeof(mData.input) : mData.sizeToBeRead), + &ret); + if (NS_FAILED(rv)) { + return rv; + } + + // Terminator: + if (ret == 0) { + *_retval = 0; + return NS_OK; + } + + mData.sizeToBeRead -= ret; + mZs.avail_in = ret; + mZs.next_in = mData.input; + } + + mZs.avail_out = aCount; + mZs.next_out = (unsigned char*)aBuffer; + + int ret = inflate(&mZs, mData.sizeToBeRead ? Z_NO_FLUSH : Z_FINISH); + if (ret != Z_BUF_ERROR && ret != Z_OK && ret != Z_STREAM_END) { + return NS_ERROR_UNEXPECTED; + } + + if (ret == Z_STREAM_END) { + mStatus = Done; + } + + *_retval = aCount - mZs.avail_out; + mData.cursor += *_retval; + return NS_OK; +} + +NS_IMETHODIMP +ArchiveInputStream::ReadSegments(nsWriteSegmentFun aWriter, + void* aClosure, + uint32_t aCount, + uint32_t* _retval) +{ + // don't have a buffer to read from, so this better not be called! + NS_NOTREACHED("Consumers should be using Read()!"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ArchiveInputStream::IsNonBlocking(bool* _retval) +{ + // We are blocking + *_retval = false; + return NS_OK; +} + +NS_IMETHODIMP +ArchiveInputStream::Seek(int32_t aWhence, int64_t aOffset) +{ + int64_t pos = aOffset; + + switch (aWhence) { + case NS_SEEK_SET: + break; + + case NS_SEEK_CUR: + pos += mData.cursor; + break; + + case NS_SEEK_END: + pos += mLength; + break; + + default: + NS_NOTREACHED("unexpected whence value"); + return NS_ERROR_UNEXPECTED; + } + + if (pos == int64_t(mData.cursor)) { + return NS_OK; + } + + if (pos < 0 || pos >= mLength) { + return NS_ERROR_FAILURE; + } + + // We have to terminate the previous operation: + nsresult rv; + if (mStatus != NotStarted) { + rv = Close(); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Reset the cursor: + mData.cursor = 0; + + // Note: This code is heavy but inflate does not have any seek() support: + uint32_t ret; + char buffer[1024]; + while (pos > 0) { + rv = Read(buffer, pos > int64_t(sizeof(buffer)) ? sizeof(buffer) : pos, &ret); + if (NS_FAILED(rv)) { + return rv; + } + + if (ret == 0) { + return NS_ERROR_UNEXPECTED; + } + + pos -= ret; + } + + return NS_OK; +} + +NS_IMETHODIMP +ArchiveInputStream::Tell(int64_t *aResult) +{ + *aResult = mData.cursor; + return NS_OK; +} + +NS_IMETHODIMP +ArchiveInputStream::SetEOF() +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +// ArchiveZipBlobImpl + +void +ArchiveZipBlobImpl::GetInternalStream(nsIInputStream** aStream, + ErrorResult& aRv) +{ + if (mLength > INT32_MAX) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + uint64_t size = mBlobImpl->GetSize(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + nsCOMPtr<nsIInputStream> inputStream; + mBlobImpl->GetInternalStream(getter_AddRefs(inputStream), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + RefPtr<ArchiveInputStream> stream = new ArchiveInputStream(size, + inputStream, + mFilename, + mStart, + mLength, + mCentral); + + stream.forget(aStream); +} + +already_AddRefed<mozilla::dom::BlobImpl> +ArchiveZipBlobImpl::CreateSlice(uint64_t aStart, + uint64_t aLength, + const nsAString& aContentType, + mozilla::ErrorResult& aRv) +{ + RefPtr<BlobImpl> impl = + new ArchiveZipBlobImpl(mFilename, mContentType, aStart, mLength, mCentral, + mBlobImpl); + return impl.forget(); +} + +NS_IMPL_ISUPPORTS_INHERITED0(ArchiveZipBlobImpl, BlobImpl) diff --git a/dom/archivereader/ArchiveZipFile.h b/dom/archivereader/ArchiveZipFile.h new file mode 100644 index 000000000..d1196486c --- /dev/null +++ b/dom/archivereader/ArchiveZipFile.h @@ -0,0 +1,81 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_archivereader_domarchivefile_h__ +#define mozilla_dom_archivereader_domarchivefile_h__ + +#include "mozilla/Attributes.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/File.h" + +#include "ArchiveReader.h" + +#include "ArchiveReaderCommon.h" +#include "zipstruct.h" + +BEGIN_ARCHIVEREADER_NAMESPACE + +/** + * ArchiveZipBlobImpl to BlobImpl + */ +class ArchiveZipBlobImpl : public BlobImplBase +{ +public: + NS_DECL_ISUPPORTS_INHERITED + + ArchiveZipBlobImpl(const nsAString& aName, + const nsAString& aContentType, + uint64_t aLength, + ZipCentral& aCentral, + BlobImpl* aBlobImpl) + : BlobImplBase(aName, aContentType, aLength), + mCentral(aCentral), + mBlobImpl(aBlobImpl), + mFilename(aName) + { + MOZ_ASSERT(mBlobImpl); + MOZ_COUNT_CTOR(ArchiveZipBlobImpl); + } + + ArchiveZipBlobImpl(const nsAString& aName, + const nsAString& aContentType, + uint64_t aStart, + uint64_t aLength, + ZipCentral& aCentral, + BlobImpl* aBlobImpl) + : BlobImplBase(aContentType, aStart, aLength), + mCentral(aCentral), + mBlobImpl(aBlobImpl), + mFilename(aName) + { + MOZ_ASSERT(mBlobImpl); + MOZ_COUNT_CTOR(ArchiveZipBlobImpl); + } + + // Overrides: + virtual void GetInternalStream(nsIInputStream** aInputStream, + ErrorResult& aRv) override; + +protected: + virtual ~ArchiveZipBlobImpl() + { + MOZ_COUNT_DTOR(ArchiveZipBlobImpl); + } + + virtual already_AddRefed<BlobImpl> + CreateSlice(uint64_t aStart, uint64_t aLength, const nsAString& aContentType, + mozilla::ErrorResult& aRv) override; + +private: // Data + ZipCentral mCentral; + RefPtr<BlobImpl> mBlobImpl; + + nsString mFilename; +}; + +END_ARCHIVEREADER_NAMESPACE + +#endif // mozilla_dom_archivereader_domarchivefile_h__ diff --git a/dom/archivereader/moz.build b/dom/archivereader/moz.build new file mode 100644 index 000000000..57dbfa121 --- /dev/null +++ b/dom/archivereader/moz.build @@ -0,0 +1,30 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +EXPORTS.mozilla.dom.archivereader += [ + 'ArchiveEvent.h', + 'ArchiveReader.h', + 'ArchiveReaderCommon.h', + 'ArchiveRequest.h', + 'ArchiveZipEvent.h', + 'ArchiveZipFile.h', +] + +UNIFIED_SOURCES += [ + 'ArchiveEvent.cpp', + 'ArchiveReader.cpp', + 'ArchiveRequest.cpp', + 'ArchiveZipEvent.cpp', + 'ArchiveZipFile.cpp', +] + +LOCAL_INCLUDES += [ + '../base', +] + +FINAL_LIBRARY = 'xul' + +MOCHITEST_MANIFESTS += ['test/mochitest.ini'] diff --git a/dom/archivereader/test/helpers.js b/dom/archivereader/test/helpers.js new file mode 100644 index 000000000..3ba430436 --- /dev/null +++ b/dom/archivereader/test/helpers.js @@ -0,0 +1,31 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator; + +function runTest() +{ + SimpleTest.waitForExplicitFinish(); + + SpecialPowers.pushPrefEnv({'set': [ ["dom.archivereader.enabled", true] ]}, function() { + SpecialPowers.createFiles(filesToCreate(), + function (files) { + testGenerator = testSteps(files); + return testGenerator.next(); + }, + function (msg) { + ok(false, "File creation error: " + msg); + finishTest(); + }); + }); +} + +function finishTest() +{ + SpecialPowers.popPrefEnv(function() { + testGenerator.close(); + SimpleTest.finish(); + }); +} diff --git a/dom/archivereader/test/mochitest.ini b/dom/archivereader/test/mochitest.ini new file mode 100644 index 000000000..6cf9e9c43 --- /dev/null +++ b/dom/archivereader/test/mochitest.ini @@ -0,0 +1,7 @@ +[DEFAULT] +support-files = + helpers.js + +[test_basic.html] +[test_nonUnicode.html] +[test_zip_in_zip.html] diff --git a/dom/archivereader/test/test_basic.html b/dom/archivereader/test/test_basic.html new file mode 100644 index 000000000..b3e5ab852 --- /dev/null +++ b/dom/archivereader/test/test_basic.html @@ -0,0 +1,227 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Archive Reader Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function filesToCreate() { + var binaryString = '504B03040A00000000002E6BF14000000000000000000000000005001C00746573742F555409000337CA055039CA055075780B' + + '000104E803000004E8030000504B03041400000008002D6BF1401780E15015000000580200000A001C00746573742F612E7478' + + '74555409000336CA05503ACA055075780B000104E803000004E8030000CB48CDC9C95728CF2FCA49E1CA18658FB2A9C4060050' + + '4B03040A00000000002F88EC40662E847010000000100000000A001C00746573742F622E74787455540900035A65FF4F42C505' + + '5075780B000104E803000004E803000068656C6C6F20776F726C642C2032210A504B01021E030A00000000002E6BF140000000' + + '000000000000000000050018000000000000001000FD4100000000746573742F555405000337CA055075780B000104E8030000' + + '04E8030000504B01021E031400000008002D6BF1401780E15015000000580200000A0018000000000001000000B4813F000000' + + '746573742F612E747874555405000336CA055075780B000104E803000004E8030000504B01021E030A00000000002F88EC4066' + + '2E847010000000100000000A0018000000000001000000B48198000000746573742F622E74787455540500035A65FF4F75780B' + + '000104E803000004E8030000504B05060000000003000300EB000000EC0000000000'; + + var binaryData = ""; + for (var i = 0, len = binaryString.length / 2; i < len; ++i) { + var hex = binaryString[i * 2] + binaryString[i * 2 + 1]; + binaryData += String.fromCharCode(parseInt(hex,16)); + } + + return [ {name: "fileArchiveReader.zip", data: binaryData}, + {name: "fileArchiveReader.txt", data: "Hello World"}]; + } + + handleFinished = 0; + function markTestDone() { + ++handleFinished; + if (isFinished()) { + finishTest(); + } + } + function isFinished() { + return handleFinished == 6; + } + + function testSteps(files) + { + var binaryFile = files[0]; + var textFile = files[1]; + + var status; + + // Create - wrong 1 + try { + var r = new ArchiveReader(); + status = false; + } + catch(e) { + status = true; + } + ok(status, "ArchiveReader() without args MUST fail"); + + // Create - wrong 2 + try { + var r = new ArchiveReader(true); + status = false; + } + catch(e) { + status = true; + } + ok(status, "ArchiveReader() without a blob arg MUST fail"); + + // Create - wrong 3 + try { + var r = new ArchiveReader("hello world"); + status = false; + } + catch(e) { + status = true; + } + ok(status, "ArchiveReader() without a blob arg MUST fail"); + + // Create - good! (but with a text file) + var rt = new ArchiveReader(textFile); + isnot(rt, null, "ArchiveReader cannot be null"); + + // GetFilename + var handle = rt.getFilenames(); + isnot(handle, null, "ArchiveReader.getFilenames() cannot be null"); + handle.onsuccess = function() { + ok(false, "ArchiveReader.getFilenames() should return a 'failure' if the input file is not a zip"); + markTestDone(); + } + handle.onerror = function() { + ok(true, "ArchiveReader.getFilenames() should return a 'error' if the input file is a zip file"); + is(this.reader, rt, "ArchiveRequest.reader == ArchiveReader"); + markTestDone(); + } + + // Create - good! + var r = new ArchiveReader(binaryFile); + isnot(r, null, "ArchiveReader cannot be null"); + + // GetFilename + handle = r.getFilenames(); + isnot(handle, null, "ArchiveReader.getFilenames() cannot be null"); + handle.onsuccess = function() { + ok(true, "ArchiveReader.getFilenames() should return a 'success'"); + is(this.result instanceof Array, true, "ArchiveReader.getFilenames() should return an array"); + is(this.result.length, 2, "ArchiveReader.getFilenames(): the array contains 2 items"); + is(this.result[0], "test/a.txt", "ArchiveReader.getFilenames(): first file is 'test/a.txt'"); + is(this.result[1], "test/b.txt", "ArchiveReader.getFilenames(): second file is 'test/b.txt'"); + ok(this.reader, r, "ArchiveRequest.reader should be == ArchiveReader"); + markTestDone(); + } + handle.onerror = function() { + ok(false, "ArchiveReader.getFilenames() should not return any 'error'"); + markTestDone(); + } + + // GetFile - wrong (no args) + try { + r.getFile(); + status = false; + } + catch(e) { + status = true; + } + ok(status, "ArchiveReader.getFile() without args fail"); + + var handle2 = r.getFile("hello world"); + isnot(handle2, null, "ArchiveReader.getFile() cannot be null"); + handle2.onsuccess = function() { + ok(false, "ArchiveReader.getFile('unknown file') should not return a 'success'"); + markTestDone(); + } + handle2.onerror = function() { + ok(true, "ArchiveReader.getFile('unknown file') should return an 'error'"); + ok(this.reader, r, "ArchiveRequest.reader should be == ArchiveReader"); + markTestDone(); + } + + var handle3 = r.getFile("test/b.txt"); + isnot(handle3, null, "ArchiveReader.getFile() cannot be null"); + handle3.onsuccess = function() { + ok(true, "ArchiveReader.getFile('test/b.txt') should return a 'success'"); + ok(this.reader, r, "ArchiveRequest.reader should be == ArchiveReader"); + is(this.result.name, "test/b.txt", "ArchiveReader.getFile('test/b.txt') the name MUST be 'test/b.txt'"); + is(this.result.type, "text/plain", "ArchiveReader.getFile('test/b.txt') the type MUST be 'text/plain'"); + + var fr = new FileReader(); + fr.readAsText(this.result); + fr.onerror = function() { + ok(false, "ArchiveReader + FileReader should work!"); + markTestDone(); + } + fr.onload = function(event) { + is(event.target.result, "hello world, 2!\n", "ArchiveReader + FileReader are working together."); + markTestDone(); + } + } + + handle3.onerror = function() { + ok(false, "ArchiveReader.getFile('test/b.txt') should not return an 'error'"); + markTestDone(); + } + + var handle4 = r.getFile("test/a.txt"); + isnot(handle4, null, "ArchiveReader.getFile() cannot be null"); + handle4.onsuccess = function() { + ok(true, "ArchiveReader.getFile('test/a.txt') should return a 'success'"); + ok(this.reader, r, "ArchiveRequest.reader should be == ArchiveReader"); + is(this.result.name, "test/a.txt", "ArchiveReader.getFile('test/a.txt') the name MUST be 'test/a.txt'"); + is(this.result.type, "text/plain", "ArchiveReader.getFile('test/a.txt') the type MUST be 'text/plain'"); + + var fr = new FileReader(); + fr.readAsText(this.result); + fr.onerror = function() { + ok(false, "ArchiveReader + FileReader should work!"); + markTestDone(); + } + fr.onload = function(event) { + is(event.target.result.length, 600, "ArchiveReader + FileReader are working with a compress data"); + var p = event.target.result.trim().split('\n'); + is(p.length, 50, "ArchiveReader + FileReader are working with a compress data"); + + for (var i = 0; i < p.length; ++i) + is(p[i], "hello world", "ArchiveReader + FileReader are working with a compress data"); + markTestDone(); + } + } + handle4.onerror = function() { + ok(false, "ArchiveReader.getFile('test/a.txt') should not return an 'error'"); + markTestDone(); + } + + var handle5 = r.getFiles(); + isnot(handle5, null, "ArchiveReader.getFiles() cannot be null"); + handle5.onsuccess = function() { + ok(true, "ArchiveReader.getFiles() should return a 'success'"); + ok(this.reader, r, "ArchiveRequest.reader should be == ArchiveReader"); + + is(this.result.length, 2, "ArchiveReader.getFilenames(): the array contains 2 items"); + is(this.result[0].name, "test/a.txt", "ArchiveReader.getFilenames(): first file is 'test/a.txt'"); + is(this.result[1].name, "test/b.txt", "ArchiveReader.getFilenames(): second file is 'test/b.txt'"); + is(this.result[0].type, "text/plain", "ArchiveReader.getFile('test/a.txt') the type MUST be 'text/plain'"); + is(this.result[1].type, "text/plain", "ArchiveReader.getFile('test/a.txt') the type MUST be 'text/plain'"); + + markTestDone(); + } + handle5.onerror = function() { + ok(false, "ArchiveReader.getFiles() should not return an 'error'"); + markTestDone(); + } + yield undefined; + } + + </script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> +</head> + +<body onload="runTest();"> +<p id="display"> +</p> +</body> + +</html> diff --git a/dom/archivereader/test/test_nonUnicode.html b/dom/archivereader/test/test_nonUnicode.html new file mode 100644 index 000000000..6237ce10f --- /dev/null +++ b/dom/archivereader/test/test_nonUnicode.html @@ -0,0 +1,77 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Archive Reader Non-Unicode Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function filesToCreate() { + var binaryData = ""; + for (var i = 0, len = binaryString.length / 2; i < len; ++i) { + var hex = binaryString[i * 2] + binaryString[i * 2 + 1]; + binaryData += String.fromCharCode(parseInt(hex,16)); + } + return [ {name: "fileArchiveReader_nonUnicode.zip", data: binaryData} ]; + } + + function test1(binaryFile) + { + var r = new ArchiveReader(binaryFile, { encoding: "ISO-8859-1" }); + isnot(r, null, "ArchiveReader cannot be null"); + + // GetFilename + var handle = r.getFilenames(); + isnot(handle, null, "ArchiveReader.getFilenames() cannot be null"); + handle.onsuccess = function() { + ok(true, "ArchiveReader.getFilenames() should return a 'success'"); + is(this.result instanceof Array, true, "ArchiveReader.getFilenames() should return an array"); + is(this.result.length, 1, "ArchiveReader.getFilenames(): the array contains 1 item"); + ok(this.reader, r, "ArchiveRequest.reader should be == ArchiveReader"); + dump('Content: ' + this.result[0] + '\n'); + test2(binaryFile); + } + } + + function test2(binaryFile) + { + try { + new ArchiveReader(binaryFile, { encoding: "random stuff" }); + ok(false, "Should have thrown for bogus encoding label."); + } catch (e) { + ok(e instanceof RangeError, "Expected a RangeError"); + } + finishTest(); + } + + function testSteps(files) + { + test1(files[0]); + yield undefined; + } + + + </script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"> +<p id="display"> +</p> +<script type="text/javascript;version=1.7"> +var binaryString = '' + +'504B0304140000000000255D094100000000000000000000000002000000912F504B03040A0000000000285D09416BB50A5A' + +'010000000100000007000000912F9B2E747874D8504B01023F00140000000000255D09410000000000000000000000000200' + +'24000000000000001000000000000000912F0A002000000000000100180062505F1A1376CD0162505F1A1376CD01FE3F8D59' + +'1176CD01504B01023F000A0000000000285D09416BB50A5A0100000001000000070024000000000000002000000020000000' + +'912F9B2E7478740A0020000000000001001800565EF41D1376CD0107BD73631176CD0107BD73631176CD01504B0506000000' + +'0002000200AD000000460000000000'; +</script> +</body> + +</html> diff --git a/dom/archivereader/test/test_zip_in_zip.html b/dom/archivereader/test/test_zip_in_zip.html new file mode 100644 index 000000000..7e381a1ef --- /dev/null +++ b/dom/archivereader/test/test_zip_in_zip.html @@ -0,0 +1,111 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Archive Reader Zip-In-Zip Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function filesToCreate() { + var binaryData = ""; + for (var i = 0, len = binaryString.length / 2; i < len; ++i) { + var hex = binaryString[i * 2] + binaryString[i * 2 + 1]; + binaryData += String.fromCharCode(parseInt(hex,16)); + } + return [ {name: "fileArchiveReader_42.zip", data: binaryData} ]; + } + + function testSteps(files) + { + var binaryFile = files[0]; + + // The input is 4 nested zip archives: + doLoop(binaryFile, 4); + + yield undefined; + } + + function doLoop(blob, loop) + { + var r = new ArchiveReader(blob); + isnot(r, null, "ArchiveReader cannot be null"); + + // GetFilename + var handle = r.getFilenames(); + isnot(handle, null, "ArchiveReader.getFilenames() cannot be null"); + handle.onsuccess = function() { + ok(true, "ArchiveReader.getFilenames() should return a 'success'"); + is(this.result instanceof Array, true, "ArchiveReader.getFilenames() should return an array"); + is(this.result.length, 1, "ArchiveReader.getFilenames(): the array contains 1 item"); + ok(this.reader, r, "ArchiveRequest.reader should be == ArchiveReader"); + + dump('Content:\n'); + for (var i = 0; i < this.result.length; ++i) + dump(' * ' + this.result[i] + '\n'); + + var handle = r.getFile(this.result[0]); + handle.onerror = function() { + ok(false, "ArchiveReader.getFile() should not return any 'error'"); + } + handle.onsuccess = function() { + ok(true, "ArchiveReader.getFilenames() should return a 'success'"); + ok(this.reader, r, "ArchiveRequest.reader should be == ArchiveReader"); + + if (loop > 0) + doLoop(this.result, loop - 1); + else + doLastLoop(this.result); + } + } + handle.onerror = function() { + ok(false, "ArchiveReader.getFilenames() should not return any 'error'"); + } + } + + function doLastLoop(blob) + { + ok(blob.size == 262144, "The last file size is wrong"); + ok(blob.name == 'empty.dat', "The last filename is wrong"); + finishTest(); + } + + </script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"> +<p id="display"> +</p> +<script type="text/javascript;version=1.7"> +var binaryString = '' + +'504B03040A0000000000B0620E415F715F15970300009703000005001C00642E7A69705554090003AC262A50AC262A507578' + +'0B000104E803000004E8030000504B03040A0000000000B0620E41CFE25F1EF7020000F702000005001C00632E7A69705554' + +'090003AC262A50AC262A5075780B000104E803000004E8030000504B03040A0000000000B0620E4107D702A4570200005702' + +'000005001C00622E7A69705554090003AC262A50AC262A5075780B000104E803000004E8030000504B03040A0000000000B0' + +'620E417E45286DB7010000B701000005001C00612E7A69705554090003AC262A50AC262A5075780B000104E803000004E803' + +'0000504B0304140000000800F7610E41784909B70F0100000000040009001C00656D7074792E646174555409000351252A50' + +'57252A5075780B000104E803000004E8030000EDC13101000000C2A0FEA9E76D07A000000000000000000000000000000000' + +'0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + +'0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + +'0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + +'0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + +'0000000000000000000000000000000000000000000000000000000000000000000000000000DE00504B01021E0314000000' + +'0800F7610E41784909B70F01000000000400090018000000000001000000B48100000000656D7074792E6461745554050003' + +'51252A5075780B000104E803000004E8030000504B050600000000010001004F000000520100000000504B01021E030A0000' + +'000000B0620E417E45286DB7010000B7010000050018000000000000000000B48100000000612E7A69705554050003AC262A' + +'5075780B000104E803000004E8030000504B050600000000010001004B000000F60100000000504B01021E030A0000000000' + +'B0620E4107D702A45702000057020000050018000000000000000000B48100000000622E7A69705554050003AC262A507578' + +'0B000104E803000004E8030000504B050600000000010001004B000000960200000000504B01021E030A0000000000B0620E' + +'41CFE25F1EF7020000F7020000050018000000000000000000B48100000000632E7A69705554050003AC262A5075780B0001' + +'04E803000004E8030000504B050600000000010001004B000000360300000000504B01021E030A0000000000B0620E415F71' + +'5F159703000097030000050018000000000000000000B48100000000642E7A69705554050003AC262A5075780B000104E803' + +'000004E8030000504B050600000000010001004B000000D60300000000'; +</script> +</body> + +</html> |