// ZipSegmentedStream.cs
// ------------------------------------------------------------------
//
// Copyright (c) 2009-2011 Dino Chiesa.
// All rights reserved.
//
// This code module is part of DotNetZip, a zipfile class library.
//
// ------------------------------------------------------------------
//
// This code is licensed under the Microsoft Public License.
// See the file License.txt for the license details.
// More info on: http://dotnetzip.codeplex.com
//
// ------------------------------------------------------------------
//
// last saved (in emacs):
// Time-stamp: <2011-July-13 22:25:45>
//
// ------------------------------------------------------------------
//
// This module defines logic for zip streams that span disk files.
//
// ------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.IO;
namespace OfficeOpenXml.Packaging.Ionic.Zip
{
internal class ZipSegmentedStream : System.IO.Stream
{
enum RwMode
{
None = 0,
ReadOnly = 1,
Write = 2,
//Update = 3
}
private RwMode rwMode;
private bool _exceptionPending; // **see note below
private string _baseName;
private string _baseDir;
//private bool _isDisposed;
private string _currentName;
private string _currentTempName;
private uint _currentDiskNumber;
private uint _maxDiskNumber;
private int _maxSegmentSize;
private System.IO.Stream _innerStream;
// **Note regarding exceptions:
//
// When ZipSegmentedStream is employed within a using clause,
// which is the typical scenario, and an exception is thrown
// within the scope of the using, Dispose() is invoked
// implicitly before processing the initial exception. If that
// happens, this class sets _exceptionPending to true, and then
// within the Dispose(bool), takes special action as
// appropriate. Need to be careful: any additional exceptions
// will mask the original one.
private ZipSegmentedStream() : base()
{
_exceptionPending = false;
}
public static ZipSegmentedStream ForReading(string name,
uint initialDiskNumber,
uint maxDiskNumber)
{
ZipSegmentedStream zss = new ZipSegmentedStream()
{
rwMode = RwMode.ReadOnly,
CurrentSegment = initialDiskNumber,
_maxDiskNumber = maxDiskNumber,
_baseName = name,
};
// Console.WriteLine("ZSS: ForReading ({0})",
// Path.GetFileName(zss.CurrentName));
zss._SetReadStream();
return zss;
}
public static ZipSegmentedStream ForWriting(string name, int maxSegmentSize)
{
ZipSegmentedStream zss = new ZipSegmentedStream()
{
rwMode = RwMode.Write,
CurrentSegment = 0,
_baseName = name,
_maxSegmentSize = maxSegmentSize,
_baseDir = Path.GetDirectoryName(name)
};
// workitem 9522
if (zss._baseDir=="") zss._baseDir=".";
zss._SetWriteStream(0);
// Console.WriteLine("ZSS: ForWriting ({0})",
// Path.GetFileName(zss.CurrentName));
return zss;
}
///
/// Sort-of like a factory method, ForUpdate is used only when
/// the application needs to update the zip entry metadata for
/// a segmented zip file, when the starting segment is earlier
/// than the ending segment, for a particular entry.
///
///
///
/// The update is always contiguous, never rolls over. As a
/// result, this method doesn't need to return a ZSS; it can
/// simply return a FileStream. That's why it's "sort of"
/// like a Factory method.
///
///
/// Caller must Close/Dispose the stream object returned by
/// this method.
///
///
public static Stream ForUpdate(string name, uint diskNumber)
{
if (diskNumber >= 99)
throw new ArgumentOutOfRangeException("diskNumber");
string fname =
String.Format("{0}.z{1:D2}",
Path.Combine(Path.GetDirectoryName(name),
Path.GetFileNameWithoutExtension(name)),
diskNumber + 1);
// Console.WriteLine("ZSS: ForUpdate ({0})",
// Path.GetFileName(fname));
// This class assumes that the update will not expand the
// size of the segment. Update is used only for an in-place
// update of zip metadata. It never will try to write beyond
// the end of a segment.
return File.Open(fname,
FileMode.Open,
FileAccess.ReadWrite,
FileShare.None);
}
public bool ContiguousWrite
{
get;
set;
}
public UInt32 CurrentSegment
{
get
{
return _currentDiskNumber;
}
private set
{
_currentDiskNumber = value;
_currentName = null; // it will get updated next time referenced
}
}
///
/// Name of the filesystem file corresponding to the current segment.
///
///
///
/// The name is not always the name currently being used in the
/// filesystem. When rwMode is RwMode.Write, the filesystem file has a
/// temporary name until the stream is closed or until the next segment is
/// started.
///
///
public String CurrentName
{
get
{
if (_currentName==null)
_currentName = _NameForSegment(CurrentSegment);
return _currentName;
}
}
public String CurrentTempName
{
get
{
return _currentTempName;
}
}
private string _NameForSegment(uint diskNumber)
{
if (diskNumber >= 99)
{
_exceptionPending = true;
throw new OverflowException("The number of zip segments would exceed 99.");
}
return String.Format("{0}.z{1:D2}",
Path.Combine(Path.GetDirectoryName(_baseName),
Path.GetFileNameWithoutExtension(_baseName)),
diskNumber + 1);
}
// Returns the segment that WILL be current if writing
// a block of the given length.
// This isn't exactly true. It could roll over beyond
// this number.
public UInt32 ComputeSegment(int length)
{
if (_innerStream.Position + length > _maxSegmentSize)
// the block will go AT LEAST into the next segment
return CurrentSegment + 1;
// it will fit in the current segment
return CurrentSegment;
}
public override String ToString()
{
return String.Format("{0}[{1}][{2}], pos=0x{3:X})",
"ZipSegmentedStream", CurrentName,
rwMode.ToString(),
this.Position);
}
private void _SetReadStream()
{
if (_innerStream != null)
{
#if NETCF
_innerStream.Close();
#else
_innerStream.Dispose();
#endif
}
if (CurrentSegment + 1 == _maxDiskNumber)
_currentName = _baseName;
// Console.WriteLine("ZSS: SRS ({0})",
// Path.GetFileName(CurrentName));
_innerStream = File.OpenRead(CurrentName);
}
///
/// Read from the stream
///
/// the buffer to read
/// the offset at which to start
/// the number of bytes to read
/// the number of bytes actually read
public override int Read(byte[] buffer, int offset, int count)
{
if (rwMode != RwMode.ReadOnly)
{
_exceptionPending = true;
throw new InvalidOperationException("Stream Error: Cannot Read.");
}
int r = _innerStream.Read(buffer, offset, count);
int r1 = r;
while (r1 != count)
{
if (_innerStream.Position != _innerStream.Length)
{
_exceptionPending = true;
throw new ZipException(String.Format("Read error in file {0}", CurrentName));
}
if (CurrentSegment + 1 == _maxDiskNumber)
return r; // no more to read
CurrentSegment++;
_SetReadStream();
offset += r1;
count -= r1;
r1 = _innerStream.Read(buffer, offset, count);
r += r1;
}
return r;
}
private void _SetWriteStream(uint increment)
{
if (_innerStream != null)
{
#if NETCF
_innerStream.Close();
#else
_innerStream.Dispose();
#endif
if (File.Exists(CurrentName))
File.Delete(CurrentName);
File.Move(_currentTempName, CurrentName);
// Console.WriteLine("ZSS: SWS close ({0})",
// Path.GetFileName(CurrentName));
}
if (increment > 0)
CurrentSegment += increment;
SharedUtilities.CreateAndOpenUniqueTempFile(_baseDir,
out _innerStream,
out _currentTempName);
// Console.WriteLine("ZSS: SWS open ({0})",
// Path.GetFileName(_currentTempName));
if (CurrentSegment == 0)
_innerStream.Write(BitConverter.GetBytes(ZipConstants.SplitArchiveSignature), 0, 4);
}
///
/// Write to the stream.
///
/// the buffer from which to write
/// the offset at which to start writing
/// the number of bytes to write
public override void Write(byte[] buffer, int offset, int count)
{
if (rwMode != RwMode.Write)
{
_exceptionPending = true;
throw new InvalidOperationException("Stream Error: Cannot Write.");
}
if (ContiguousWrite)
{
// enough space for a contiguous write?
if (_innerStream.Position + count > _maxSegmentSize)
_SetWriteStream(1);
}
else
{
while (_innerStream.Position + count > _maxSegmentSize)
{
int c = unchecked(_maxSegmentSize - (int)_innerStream.Position);
_innerStream.Write(buffer, offset, c);
_SetWriteStream(1);
count -= c;
offset += c;
}
}
_innerStream.Write(buffer, offset, count);
}
public long TruncateBackward(uint diskNumber, long offset)
{
// Console.WriteLine("***ZSS.Trunc to disk {0}", diskNumber);
// Console.WriteLine("***ZSS.Trunc: current disk {0}", CurrentSegment);
if (diskNumber >= 99)
throw new ArgumentOutOfRangeException("diskNumber");
if (rwMode != RwMode.Write)
{
_exceptionPending = true;
throw new ZipException("bad state.");
}
// Seek back in the segmented stream to a (maybe) prior segment.
// Check if it is the same segment. If it is, very simple.
if (diskNumber == CurrentSegment)
{
var x =_innerStream.Seek(offset, SeekOrigin.Begin);
// workitem 10178
Ionic.Zip.SharedUtilities.Workaround_Ladybug318918(_innerStream);
return x;
}
// Seeking back to a prior segment.
// The current segment and any intervening segments must be removed.
// First, close the current segment, and then remove it.
if (_innerStream != null)
{
#if NETCF
_innerStream.Close();
#else
_innerStream.Dispose();
#endif
if (File.Exists(_currentTempName))
File.Delete(_currentTempName);
}
// Now, remove intervening segments.
for (uint j= CurrentSegment-1; j > diskNumber; j--)
{
string s = _NameForSegment(j);
// Console.WriteLine("***ZSS.Trunc: removing file {0}", s);
if (File.Exists(s))
File.Delete(s);
}
// now, open the desired segment. It must exist.
CurrentSegment = diskNumber;
// get a new temp file, try 3 times:
for (int i = 0; i < 3; i++)
{
try
{
_currentTempName = SharedUtilities.InternalGetTempFileName();
// move the .z0x file back to a temp name
File.Move(CurrentName, _currentTempName);
break; // workitem 12403
}
catch(IOException)
{
if (i == 2) throw;
}
}
// open it
_innerStream = new FileStream(_currentTempName, FileMode.Open);
var r = _innerStream.Seek(offset, SeekOrigin.Begin);
// workitem 10178
Ionic.Zip.SharedUtilities.Workaround_Ladybug318918(_innerStream);
return r;
}
public override bool CanRead
{
get
{
return (rwMode == RwMode.ReadOnly &&
(_innerStream != null) &&
_innerStream.CanRead);
}
}
public override bool CanSeek
{
get
{
return (_innerStream != null) &&
_innerStream.CanSeek;
}
}
public override bool CanWrite
{
get
{
return (rwMode == RwMode.Write) &&
(_innerStream != null) &&
_innerStream.CanWrite;
}
}
public override void Flush()
{
_innerStream.Flush();
}
public override long Length
{
get
{
return _innerStream.Length;
}
}
public override long Position
{
get { return _innerStream.Position; }
set { _innerStream.Position = value; }
}
public override long Seek(long offset, System.IO.SeekOrigin origin)
{
var x = _innerStream.Seek(offset, origin);
// workitem 10178
Ionic.Zip.SharedUtilities.Workaround_Ladybug318918(_innerStream);
return x;
}
public override void SetLength(long value)
{
if (rwMode != RwMode.Write)
{
_exceptionPending = true;
throw new InvalidOperationException();
}
_innerStream.SetLength(value);
}
protected override void Dispose(bool disposing)
{
// this gets called by Stream.Close()
// if (_isDisposed) return;
// _isDisposed = true;
//Console.WriteLine("Dispose (mode={0})\n", rwMode.ToString());
try
{
if (_innerStream != null)
{
#if NETCF
_innerStream.Close();
#else
_innerStream.Dispose();
#endif
//_innerStream = null;
if (rwMode == RwMode.Write)
{
if (_exceptionPending)
{
// possibly could try to clean up all the
// temp files created so far...
}
else
{
// // move the final temp file to the .zNN name
// if (File.Exists(CurrentName))
// File.Delete(CurrentName);
// if (File.Exists(_currentTempName))
// File.Move(_currentTempName, CurrentName);
}
}
}
}
finally
{
base.Dispose(disposing);
}
}
}
}