using System;
using System.IO;
namespace ICSharpCode.SharpZipLib.Tar
{
///
/// The TarOutputStream writes a UNIX tar archive as an OutputStream.
/// Methods are provided to put entries, and then write their contents
/// by writing to this stream using write().
///
/// public
public class TarOutputStream : Stream
{
#region Constructors
///
/// Construct TarOutputStream using default block factor
///
/// stream to write to
public TarOutputStream(Stream outputStream)
: this(outputStream, TarBuffer.DefaultBlockFactor)
{
}
///
/// Construct TarOutputStream with user specified block factor
///
/// stream to write to
/// blocking factor
public TarOutputStream(Stream outputStream, int blockFactor)
{
if (outputStream == null) {
throw new ArgumentNullException("nameof(outputStream)");
}
this.outputStream = outputStream;
buffer = TarBuffer.CreateOutputTarBuffer(outputStream, blockFactor);
assemblyBuffer = new byte[TarBuffer.BlockSize];
blockBuffer = new byte[TarBuffer.BlockSize];
}
#endregion
///
/// Gets or sets a flag indicating ownership of underlying stream.
/// When the flag is true will close the underlying stream also.
///
/// The default value is true.
public bool IsStreamOwner {
get { return buffer.IsStreamOwner; }
set { buffer.IsStreamOwner = value; }
}
///
/// true if the stream supports reading; otherwise, false.
///
public override bool CanRead {
get {
return outputStream.CanRead;
}
}
///
/// true if the stream supports seeking; otherwise, false.
///
public override bool CanSeek {
get {
return outputStream.CanSeek;
}
}
///
/// true if stream supports writing; otherwise, false.
///
public override bool CanWrite {
get {
return outputStream.CanWrite;
}
}
///
/// length of stream in bytes
///
public override long Length {
get {
return outputStream.Length;
}
}
///
/// gets or sets the position within the current stream.
///
public override long Position {
get {
return outputStream.Position;
}
set {
outputStream.Position = value;
}
}
///
/// set the position within the current stream
///
/// The offset relative to the to seek to
/// The to seek from.
/// The new position in the stream.
public override long Seek(long offset, SeekOrigin origin)
{
return outputStream.Seek(offset, origin);
}
///
/// Set the length of the current stream
///
/// The new stream length.
public override void SetLength(long value)
{
outputStream.SetLength(value);
}
///
/// Read a byte from the stream and advance the position within the stream
/// by one byte or returns -1 if at the end of the stream.
///
/// The byte value or -1 if at end of stream
public override int ReadByte()
{
return outputStream.ReadByte();
}
///
/// read bytes from the current stream and advance the position within the
/// stream by the number of bytes read.
///
/// The buffer to store read bytes in.
/// The index into the buffer to being storing bytes at.
/// The desired number of bytes to read.
/// The total number of bytes read, or zero if at the end of the stream.
/// The number of bytes may be less than the count
/// requested if data is not avialable.
public override int Read(byte[] buffer, int offset, int count)
{
return outputStream.Read(buffer, offset, count);
}
///
/// All buffered data is written to destination
///
public override void Flush()
{
outputStream.Flush();
}
///
/// Ends the TAR archive without closing the underlying OutputStream.
/// The result is that the EOF block of nulls is written.
///
public void Finish()
{
if (IsEntryOpen) {
CloseEntry();
}
WriteEofBlock();
}
///
/// Ends the TAR archive and closes the underlying OutputStream.
///
/// This means that Finish() is called followed by calling the
/// TarBuffer's Close().
protected override void Dispose(bool disposing)
{
if (!isClosed) {
isClosed = true;
Finish();
buffer.Close();
}
}
///
/// Get the record size being used by this stream's TarBuffer.
///
public int RecordSize {
get { return buffer.RecordSize; }
}
///
/// Get the record size being used by this stream's TarBuffer.
///
///
/// The TarBuffer record size.
///
[Obsolete("Use RecordSize property instead")]
public int GetRecordSize()
{
return buffer.RecordSize;
}
///
/// Get a value indicating wether an entry is open, requiring more data to be written.
///
bool IsEntryOpen {
get { return (currBytes < currSize); }
}
///
/// Put an entry on the output stream. This writes the entry's
/// header and positions the output stream for writing
/// the contents of the entry. Once this method is called, the
/// stream is ready for calls to write() to write the entry's
/// contents. Once the contents are written, closeEntry()
/// MUST be called to ensure that all buffered data
/// is completely written to the output stream.
///
///
/// The TarEntry to be written to the archive.
///
public void PutNextEntry(TarEntry entry)
{
if (entry == null) {
throw new ArgumentNullException("nameof(entry)");
}
if (entry.TarHeader.Name.Length > TarHeader.NAMELEN) {
var longHeader = new TarHeader();
longHeader.TypeFlag = TarHeader.LF_GNU_LONGNAME;
longHeader.Name = longHeader.Name + "././@LongLink";
longHeader.Mode = 420;//644 by default
longHeader.UserId = entry.UserId;
longHeader.GroupId = entry.GroupId;
longHeader.GroupName = entry.GroupName;
longHeader.UserName = entry.UserName;
longHeader.LinkName = "";
longHeader.Size = entry.TarHeader.Name.Length + 1; // Plus one to avoid dropping last char
longHeader.WriteHeader(blockBuffer);
buffer.WriteBlock(blockBuffer); // Add special long filename header block
int nameCharIndex = 0;
while (nameCharIndex < entry.TarHeader.Name.Length + 1 /* we've allocated one for the null char, now we must make sure it gets written out */) {
Array.Clear(blockBuffer, 0, blockBuffer.Length);
TarHeader.GetAsciiBytes(entry.TarHeader.Name, nameCharIndex, this.blockBuffer, 0, TarBuffer.BlockSize); // This func handles OK the extra char out of string length
nameCharIndex += TarBuffer.BlockSize;
buffer.WriteBlock(blockBuffer);
}
}
entry.WriteEntryHeader(blockBuffer);
buffer.WriteBlock(blockBuffer);
currBytes = 0;
currSize = entry.IsDirectory ? 0 : entry.Size;
}
///
/// Close an entry. This method MUST be called for all file
/// entries that contain data. The reason is that we must
/// buffer data written to the stream in order to satisfy
/// the buffer's block based writes. Thus, there may be
/// data fragments still being assembled that must be written
/// to the output stream before this entry is closed and the
/// next entry written.
///
public void CloseEntry()
{
if (assemblyBufferLength > 0) {
Array.Clear(assemblyBuffer, assemblyBufferLength, assemblyBuffer.Length - assemblyBufferLength);
buffer.WriteBlock(assemblyBuffer);
currBytes += assemblyBufferLength;
assemblyBufferLength = 0;
}
if (currBytes < currSize) {
string errorText = string.Format(
"Entry closed at '{0}' before the '{1}' bytes specified in the header were written",
currBytes, currSize);
throw new TarException(errorText);
}
}
///
/// Writes a byte to the current tar archive entry.
/// This method simply calls Write(byte[], int, int).
///
///
/// The byte to be written.
///
public override void WriteByte(byte value)
{
Write(new byte[] { value }, 0, 1);
}
///
/// Writes bytes to the current tar archive entry. This method
/// is aware of the current entry and will throw an exception if
/// you attempt to write bytes past the length specified for the
/// current entry. The method is also (painfully) aware of the
/// record buffering required by TarBuffer, and manages buffers
/// that are not a multiple of recordsize in length, including
/// assembling records from small buffers.
///
///
/// The buffer to write to the archive.
///
///
/// The offset in the buffer from which to get bytes.
///
///
/// The number of bytes to write.
///
public override void Write(byte[] buffer, int offset, int count)
{
if (buffer == null) {
throw new ArgumentNullException("nameof(buffer)");
}
if (offset < 0) {
throw new ArgumentOutOfRangeException("nameof(offset)", "Cannot be negative");
}
if (buffer.Length - offset < count) {
throw new ArgumentException("offset and count combination is invalid");
}
if (count < 0) {
throw new ArgumentOutOfRangeException("nameof(count)", "Cannot be negative");
}
if ((currBytes + count) > currSize) {
string errorText = string.Format("request to write '{0}' bytes exceeds size in header of '{1}' bytes",
count, this.currSize);
throw new ArgumentOutOfRangeException("nameof(count)", errorText);
}
//
// We have to deal with assembly!!!
// The programmer can be writing little 32 byte chunks for all
// we know, and we must assemble complete blocks for writing.
// TODO REVIEW Maybe this should be in TarBuffer? Could that help to
// eliminate some of the buffer copying.
//
if (assemblyBufferLength > 0) {
if ((assemblyBufferLength + count) >= blockBuffer.Length) {
int aLen = blockBuffer.Length - assemblyBufferLength;
Array.Copy(assemblyBuffer, 0, blockBuffer, 0, assemblyBufferLength);
Array.Copy(buffer, offset, blockBuffer, assemblyBufferLength, aLen);
this.buffer.WriteBlock(blockBuffer);
currBytes += blockBuffer.Length;
offset += aLen;
count -= aLen;
assemblyBufferLength = 0;
} else {
Array.Copy(buffer, offset, assemblyBuffer, assemblyBufferLength, count);
offset += count;
assemblyBufferLength += count;
count -= count;
}
}
//
// When we get here we have EITHER:
// o An empty "assembly" buffer.
// o No bytes to write (count == 0)
//
while (count > 0) {
if (count < blockBuffer.Length) {
Array.Copy(buffer, offset, assemblyBuffer, assemblyBufferLength, count);
assemblyBufferLength += count;
break;
}
this.buffer.WriteBlock(buffer, offset);
int bufferLength = blockBuffer.Length;
currBytes += bufferLength;
count -= bufferLength;
offset += bufferLength;
}
}
///
/// Write an EOF (end of archive) block to the tar archive.
/// The end of the archive is indicated by two blocks consisting entirely of zero bytes.
///
void WriteEofBlock()
{
Array.Clear(blockBuffer, 0, blockBuffer.Length);
buffer.WriteBlock(blockBuffer);
buffer.WriteBlock(blockBuffer);
}
#region Instance Fields
///
/// bytes written for this entry so far
///
long currBytes;
///
/// current 'Assembly' buffer length
///
int assemblyBufferLength;
///
/// Flag indicating wether this instance has been closed or not.
///
bool isClosed;
///
/// Size for the current entry
///
protected long currSize;
///
/// single block working buffer
///
protected byte[] blockBuffer;
///
/// 'Assembly' buffer used to assemble data before writing
///
protected byte[] assemblyBuffer;
///
/// TarBuffer used to provide correct blocking factor
///
protected TarBuffer buffer;
///
/// the destination stream for the archive contents
///
protected Stream outputStream;
#endregion
}
}