/// <summary> /// Reads, decrypts and returns the header of an entity /// </summary> /// <returns></returns> /// <exception cref="InvalidArchiveException"></exception> private JpsEntityDescriptionBlockData ReadFileHeader() { JpsEntityDescriptionBlockHeader preamble = new JpsEntityDescriptionBlockHeader(); JpsEntityDescriptionBlockData fileHeader = new JpsEntityDescriptionBlockData(); // Read the entity description block header preamble.Signature = ReadAsciiString(3); if (preamble.Signature != "JPF") { throw new InvalidArchiveException( String.Format(Language.ResourceManager.GetString("ERR_FORMAT_INVALID_HEADER_AT_POSITION"), CurrentPartNumber, Progress.FilePosition) ); } preamble.EncryptedSize = ReadUShort(); preamble.DecryptedSize = ReadUShort(); // Read the encrypted data using (MemoryStream encryptedSteam = ReadIntoStream(preamble.EncryptedSize)) { using (MemoryStream decryptedStream = Decrypt(encryptedSteam)) { if (decryptedStream.Length != preamble.DecryptedSize) { throw new InvalidArchiveException( String.Format( Language.ResourceManager.GetString("ERR_FORMAT_JPS_DECRYPTED_SIZE_DIFFERENCE"), preamble.DecryptedSize, decryptedStream.Length) ); } fileHeader.PathLength = ReadUShort(decryptedStream); fileHeader.Path = ReadUtf8String(fileHeader.PathLength, decryptedStream); switch (ReadByte(decryptedStream)) { case 0: fileHeader.EntityType = TEntityType.Directory; break; case 1: fileHeader.EntityType = TEntityType.File; break; case 2: fileHeader.EntityType = TEntityType.Symlink; break; default: throw new InvalidArchiveException(Language.ResourceManager.GetString("ERR_FORMAT_INVALID_ENTITY_TYPE")); //break; } switch (ReadByte(decryptedStream)) { case 0: fileHeader.CompressionType = TCompressionType.Uncompressed; break; case 1: fileHeader.CompressionType = TCompressionType.GZip; break; case 2: fileHeader.CompressionType = TCompressionType.BZip2; break; default: throw new InvalidArchiveException(Language.ResourceManager.GetString("ERR_FORMAT_INVALID_COMPRESSION_METHOD")); //break; } fileHeader.UncompressedSize = ReadULong(decryptedStream); fileHeader.Permissions = ReadULong(decryptedStream); fileHeader.FileModificationTime = ReadULong(decryptedStream); } } // Invoke the OnEntityEvent event. We need to do some work to get there, through... // -- Create a new archive information record EntityInformation info = new EntityInformation(); // -- Incorporate bits from the file header info.CompressedSize = 0; // JPS does not report the compressed size. We have to read a chunk at a time. info.UncompressedSize = fileHeader.UncompressedSize; info.StoredName = fileHeader.Path; // -- Get the absolute path of the file info.AbsoluteName = ""; if (DataWriter != null) { info.AbsoluteName = DataWriter.GetAbsoluteFilePath(fileHeader.Path); } // -- Get some bits from the currently open archive file info.PartNumber = CurrentPartNumber ?? 1; info.PartOffset = InputStream.Position; // -- Create the event arguments object EntityEventArgs args = new EntityEventArgs(info); // -- Finally, invoke the event OnEntityEvent(args); return(fileHeader); }
/// <summary> /// Processes a data block in a JPA file located in the current file position /// </summary> /// <param name="dataBlockHeader">The header of the block being processed</param> /// <param name="token">A cancellation token, allowing the called to cancel the processing</param> private void ProcessDataBlock(JpsEntityDescriptionBlockData dataBlockHeader, CancellationToken token) { // Update the archive's progress record Progress.FilePosition = (ulong)(SizesOfPartsAlreadyRead + InputStream.Position); Progress.Status = ExtractionStatus.Running; // Create the event arguments we'll use when invoking the event ProgressEventArgs args = new ProgressEventArgs(Progress); switch (dataBlockHeader.EntityType) { // Just a directory? Create it and return. case TEntityType.Directory: if (DataWriter != null) { DataWriter.MakeDirRecursive(DataWriter.GetAbsoluteFilePath(dataBlockHeader.Path)); } break; // Symlink? Read the encrypted target and create the symlink case TEntityType.Symlink: using (MemoryStream source = ReadAndDecryptNextDataChunkBlock()) { string symlinkTarget = ReadUtf8String((int)dataBlockHeader.UncompressedSize); if (DataWriter != null) { DataWriter.MakeSymlink(symlinkTarget, DataWriter.GetAbsoluteFilePath(dataBlockHeader.Path)); } } Progress.RunningUncompressed += dataBlockHeader.UncompressedSize; break; // We have a file. Now it's more complicated. We have to read through and process each chunk until // we have reached the decompressed size. case TEntityType.File: ulong currentRunningDecompressed = 0; if (DataWriter != null) { DataWriter.StartFile(dataBlockHeader.Path); } while (currentRunningDecompressed < dataBlockHeader.UncompressedSize) { // First chance to cancel: before decompressing data if (token.IsCancellationRequested) { token.ThrowIfCancellationRequested(); } using (MemoryStream decryptedStream = ReadAndDecryptNextDataChunkBlock()) { // Second chance to cancel: after decompressing data, before decompressing / writing data if (token.IsCancellationRequested) { token.ThrowIfCancellationRequested(); } switch (dataBlockHeader.CompressionType) { case TCompressionType.GZip: using (DeflateStream decompressStream = new DeflateStream(decryptedStream, CompressionMode.Decompress)) { // We need to decompress the data to get its length using (MemoryStream sourceStream = new MemoryStream()) { decompressStream.CopyTo(sourceStream); sourceStream.Seek(0, SeekOrigin.Begin); ulong sourceStreamLength = (ulong)sourceStream.Length; currentRunningDecompressed += sourceStreamLength; if (DataWriter != null) { DataWriter.WriteData(sourceStream); } } } break; case TCompressionType.BZip2: using (BZip2InputStream decompressStream = new BZip2InputStream(decryptedStream)) { // We need to decompress the data to get its length using (MemoryStream sourceStream = new MemoryStream()) { decompressStream.CopyTo(sourceStream); sourceStream.Seek(0, SeekOrigin.Begin); ulong sourceStreamLength = (ulong)sourceStream.Length; currentRunningDecompressed += sourceStreamLength; if (DataWriter != null) { DataWriter.WriteData(sourceStream); } } } break; case TCompressionType.Uncompressed: currentRunningDecompressed += (ulong)decryptedStream.Length; if (DataWriter != null) { DataWriter.WriteData(decryptedStream); } break; } } } Progress.RunningUncompressed += currentRunningDecompressed; Progress.FilePosition = (ulong)(SizesOfPartsAlreadyRead + InputStream.Position); if (DataWriter != null) { DataWriter.StopFile(); } break; } }
/// <summary> /// Implements the Extract method which extracts a backup archive. A DataWriter must be already assigned and configured or an /// exception will be raised. /// </summary> public override void Extract(CancellationToken token) { // Initialize Close(); Progress.FilePosition = 0; Progress.RunningCompressed = 0; Progress.RunningUncompressed = 0; Progress.Status = ExtractionStatus.Running; Progress.LastException = null; ProgressEventArgs args; JpsHeaderData archiveHeader; if (_legacyKey == null) { throw new InvalidArchiveException(Language.ResourceManager.GetString("ERR_FORMAT_JPS_EMPTY_PASSWORD")); } try { // Read the archive header archiveHeader = ReadArchiveHeader(); // Invoke event at start of extraction args = new ProgressEventArgs(Progress); OnProgressEvent(args); // The end of archive is 18 bytes before the end of the archive due to the End of Archive record while ((CurrentPartNumber != null) && (Progress.FilePosition < (archiveHeader.TotalSize - 18))) { JpsEntityDescriptionBlockData fileHeader = ReadFileHeader(); if (token.IsCancellationRequested) { token.ThrowIfCancellationRequested(); } ProcessDataBlock(fileHeader, token); args = new ProgressEventArgs(Progress); OnProgressEvent(args); } } catch (OperationCanceledException cancelledException) { // The operation was cancelled. Set the state to Idle and reset the extraction. Close(); Progress.FilePosition = 0; Progress.RunningCompressed = 0; Progress.RunningUncompressed = 0; Progress.Status = ExtractionStatus.Idle; Progress.LastException = cancelledException; // Invoke an event notifying susbcribers about the cancelation. args = new ProgressEventArgs(Progress); OnProgressEvent(args); return; } catch (Exception errorException) { // Any other exception. Close the option file and set the status to error. Close(); Progress.Status = ExtractionStatus.Error; Progress.LastException = errorException; // Invoke an event notifying of the error state args = new ProgressEventArgs(Progress); OnProgressEvent(args); return; } // Invoke an event signaling the end of extraction Progress.Status = ExtractionStatus.Finished; args = new ProgressEventArgs(Progress); OnProgressEvent(args); }