3 // Copyright (C) 2001 Mike Krueger
5 // This program is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU General Public License
7 // as published by the Free Software Foundation; either version 2
8 // of the License, or (at your option) any later version.
10 // This program is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with this program; if not, write to the Free Software
17 // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19 // Linking this library statically or dynamically with other modules is
20 // making a combined work based on this library. Thus, the terms and
21 // conditions of the GNU General Public License cover the whole
24 // As a special exception, the copyright holders of this library give you
25 // permission to link this library with independent modules to produce an
26 // executable, regardless of the license terms of these independent
27 // modules, and to copy and distribute the resulting executable under
28 // terms of your choice, provided that you also meet, for each linked
29 // independent module, the terms and conditions of the license of that
30 // module. An independent module is a module which is not derived from
31 // or based on this library. If you modify this library, you may extend
32 // this exception to your version of the library, but you are not
33 // obligated to do so. If you do not wish to do so, delete this
34 // exception statement from your version.
40 namespace ICSharpCode.SharpZipLib.Silverlight.Tar
43 /// Used to advise clients of 'events' while processing archives
45 public delegate void ProgressMessageHandler(TarArchive archive, TarEntry entry, string message);
48 /// The TarArchive class implements the concept of a
49 /// 'Tape Archive'. A tar archive is a series of entries, each of
50 /// which represents a file system object. Each entry in
51 /// the archive consists of a header block followed by 0 or more data blocks.
52 /// Directory entries consist only of the header block, and are followed by entries
53 /// for the directory's contents. File entries consist of a
54 /// header followed by the number of blocks needed to
55 /// contain the file's contents. All entries are written on
56 /// block boundaries. Blocks are 512 bytes long.
58 /// TarArchives are instantiated in either read or write mode,
59 /// based upon whether they are instantiated with an InputStream
60 /// or an OutputStream. Once instantiated TarArchives read/write
61 /// mode can not be changed.
63 /// There is currently no support for random access to tar archives.
64 /// However, it seems that subclassing TarArchive, and using the
65 /// TarBuffer.CurrentRecord and TarBuffer.CurrentBlock
66 /// properties, this would be rather trivial.
68 public class TarArchive : IDisposable
71 /// Get/set the ascii file translation flag. If ascii file translation
72 /// is true, then the file is checked to see if it a binary file or not.
73 /// If the flag is true and the test indicates it is ascii text
74 /// file, it will be translated. The translation converts the local
75 /// operating system's concept of line ends into the UNIX line end,
76 /// '\n', which is the defacto standard for a TAR archive. This makes
77 /// text files compatible with UNIX.
79 public bool AsciiTranslate
85 throw new ObjectDisposedException("TarArchive");
88 return _asciiTranslate;
95 throw new ObjectDisposedException("TarArchive");
98 _asciiTranslate = value;
103 /// PathPrefix is added to entry names as they are written if the value is not null.
104 /// A slash character is appended after PathPrefix
106 public string PathPrefix
112 throw new ObjectDisposedException("TarArchive");
122 throw new ObjectDisposedException("TarArchive");
130 /// RootPath is removed from entry names if it is found at the
131 /// beginning of the name.
133 public string RootPath
139 throw new ObjectDisposedException("TarArchive");
149 throw new ObjectDisposedException("TarArchive");
157 /// Get or set a value indicating if overrides defined by <see cref="SetUserInfo">SetUserInfo</see> should be applied.
159 /// <remarks>If overrides are not applied then the values as set in each header will be used.</remarks>
160 public bool ApplyUserInfoOverrides
166 throw new ObjectDisposedException("TarArchive");
169 return _applyUserInfoOverrides;
176 throw new ObjectDisposedException("TarArchive");
179 _applyUserInfoOverrides = value;
184 /// Get the archive user id.
185 /// See <see cref="ApplyUserInfoOverrides">ApplyUserInfoOverrides</see> for detail
186 /// on how to allow setting values on a per entry basis.
189 /// The current user id.
197 throw new ObjectDisposedException("TarArchive");
205 /// Get the archive user name.
206 /// See <see cref="ApplyUserInfoOverrides">ApplyUserInfoOverrides</see> for detail
207 /// on how to allow setting values on a per entry basis.
210 /// The current user name.
212 public string UserName
218 throw new ObjectDisposedException("TarArchive");
226 /// Get the archive group id.
227 /// See <see cref="ApplyUserInfoOverrides">ApplyUserInfoOverrides</see> for detail
228 /// on how to allow setting values on a per entry basis.
231 /// The current group id.
239 throw new ObjectDisposedException("TarArchive");
247 /// Get the archive group name.
248 /// See <see cref="ApplyUserInfoOverrides">ApplyUserInfoOverrides</see> for detail
249 /// on how to allow setting values on a per entry basis.
252 /// The current group name.
254 public string GroupName
260 throw new ObjectDisposedException("TarArchive");
268 /// Get the archive's record size. Tar archives are composed of
269 /// a series of RECORDS each containing a number of BLOCKS.
270 /// This allowed tar archives to match the IO characteristics of
271 /// the physical device being used. Archives are expected
272 /// to be properly "blocked".
275 /// The record size this archive is using.
277 public int RecordSize
283 throw new ObjectDisposedException("TarArchive");
288 return tarIn.RecordSize;
292 return tarOut.RecordSize;
294 return TarBuffer.DefaultRecordSize;
298 #region IDisposable Members
300 void IDisposable.Dispose()
308 /// Client hook allowing detailed information to be reported during processing
310 public event ProgressMessageHandler ProgressMessageEvent;
313 /// Raises the ProgressMessage event
315 /// <param name="entry">The <see cref="TarEntry">TarEntry</see> for this event</param>
316 /// <param name="message">message for this event. Null is no message</param>
317 protected virtual void OnProgressMessageEvent(TarEntry entry, string message)
319 if (ProgressMessageEvent != null)
321 ProgressMessageEvent(this, entry, message);
326 /// Set the flag that determines whether existing files are
327 /// kept, or overwritten during extraction.
329 /// <param name="keepOldFiles">
330 /// If true, do not overwrite existing files.
332 public void SetKeepOldFiles(bool keepOldFiles)
336 throw new ObjectDisposedException("TarArchive");
338 _keepOldFiles = keepOldFiles;
342 /// Set the ascii file translation flag.
344 /// <param name= "asciiTranslate">
345 /// If true, translate ascii text files.
347 [Obsolete("Use the AsciiTranslate property")]
348 public void SetAsciiTranslation(bool asciiTranslate)
352 throw new ObjectDisposedException("TarArchive");
355 _asciiTranslate = asciiTranslate;
359 /// Set user and group information that will be used to fill in the
360 /// tar archive's entry headers. This information based on that available
361 /// for the linux operating system, which is not always available on other
362 /// operating systems. TarArchive allows the programmer to specify values
363 /// to be used in their place.
364 /// <see cref="ApplyUserInfoOverrides"/> is set to true by this call.
366 /// <param name="userId">
367 /// The user id to use in the headers.
369 /// <param name="userName">
370 /// The user name to use in the headers.
372 /// <param name="groupId">
373 /// The group id to use in the headers.
375 /// <param name="groupName">
376 /// The group name to use in the headers.
378 public void SetUserInfo(int userId, string userName, int groupId, string groupName)
382 throw new ObjectDisposedException("TarArchive");
386 _userName = userName;
388 _groupName = groupName;
389 _applyUserInfoOverrides = true;
393 /// Close the archive.
395 [Obsolete("Use Close instead")]
396 public void CloseArchive()
402 /// Perform the "list" command for the archive contents.
404 /// NOTE That this method uses the <see cref="ProgressMessageEvent"> progress event</see> to actually list
405 /// the contents. If the progress display event is not set, nothing will be listed!
407 public void ListContents()
411 throw new ObjectDisposedException("TarArchive");
416 var entry = tarIn.GetNextEntry();
422 OnProgressMessageEvent(entry, null);
427 /// Perform the "extract" command and extract the contents of the archive.
429 /// <param name="destinationDirectory">
430 /// The destination directory into which to extract.
432 public void ExtractContents(string destinationDirectory)
436 throw new ObjectDisposedException("TarArchive");
441 var entry = tarIn.GetNextEntry();
448 ExtractEntry(destinationDirectory, entry);
453 /// Extract an entry from the archive. This method assumes that the
454 /// tarIn stream has been properly set with a call to GetNextEntry().
456 /// <param name="destDir">
457 /// The destination directory into which to extract.
459 /// <param name="entry">
460 /// The TarEntry returned by tarIn.GetNextEntry().
462 private void ExtractEntry(string destDir, TarEntry entry)
464 OnProgressMessageEvent(entry, null);
466 var name = entry.Name;
468 if (Path.IsPathRooted(name))
471 // for UNC names... \\machine\share\zoom\beet.txt gives \zoom\beet.txt
472 name = name.Substring(Path.GetPathRoot(name).Length);
475 name = name.Replace('/', Path.DirectorySeparatorChar);
477 var destFile = Path.Combine(destDir, name);
479 if (entry.IsDirectory)
481 EnsureDirectoryExists(destFile);
485 var parentDirectory = Path.GetDirectoryName(destFile);
486 EnsureDirectoryExists(parentDirectory);
489 var fileInfo = new FileInfo(destFile);
494 OnProgressMessageEvent(entry, "Destination file already exists");
497 else if ((fileInfo.Attributes & FileAttributes.ReadOnly) != 0)
499 OnProgressMessageEvent(entry, "Destination file already exists, and is read-only");
506 var asciiTrans = false;
508 Stream outputStream = File.Create(destFile);
511 asciiTrans = !IsBinary(destFile);
514 StreamWriter outw = null;
517 outw = new StreamWriter(outputStream);
520 var rdbuf = new byte[32*1024];
524 var numRead = tarIn.Read(rdbuf, 0, rdbuf.Length);
533 for (int off = 0, b = 0; b < numRead; ++b)
537 var s = Encoding.UTF8.GetString(rdbuf, off, (b - off));
545 outputStream.Write(rdbuf, 0, numRead);
555 outputStream.Close();
562 /// Write an entry to the archive. This method will call the putNextEntry
563 /// and then write the contents of the entry, and finally call closeEntry()
564 /// for entries that are files. For directories, it will call putNextEntry(),
565 /// and then, if the recurse flag is true, process each entry that is a
566 /// child of the directory.
568 /// <param name="sourceEntry">
569 /// The TarEntry representing the entry to write to the archive.
571 /// <param name="recurse">
572 /// If true, process the children of directory entries.
574 public void WriteEntry(TarEntry sourceEntry, bool recurse)
576 if (sourceEntry == null)
578 throw new ArgumentNullException("sourceEntry");
583 throw new ObjectDisposedException("TarArchive");
590 TarHeader.SetValueDefaults(sourceEntry.UserId, sourceEntry.UserName,
591 sourceEntry.GroupId, sourceEntry.GroupName);
593 InternalWriteEntry(sourceEntry, recurse);
599 TarHeader.RestoreSetValues();
605 /// Write an entry to the archive. This method will call the putNextEntry
606 /// and then write the contents of the entry, and finally call closeEntry()
607 /// for entries that are files. For directories, it will call putNextEntry(),
608 /// and then, if the recurse flag is true, process each entry that is a
609 /// child of the directory.
611 /// <param name="sourceEntry">
612 /// The TarEntry representing the entry to write to the archive.
614 /// <param name="recurse">
615 /// If true, process the children of directory entries.
617 private void InternalWriteEntry(TarEntry sourceEntry, bool recurse)
619 string tempFileName = null;
620 var entryFilename = sourceEntry.File;
622 var entry = (TarEntry) sourceEntry.Clone();
624 if (_applyUserInfoOverrides)
626 entry.GroupId = _groupId;
627 entry.GroupName = _groupName;
628 entry.UserId = _userId;
629 entry.UserName = _userName;
632 OnProgressMessageEvent(entry, null);
634 if (_asciiTranslate && !entry.IsDirectory)
636 var asciiTrans = !IsBinary(entryFilename);
640 tempFileName = Path.GetTempFileName();
642 using (var inStream = File.OpenText(entryFilename))
644 using (Stream outStream = File.Create(tempFileName))
648 var line = inStream.ReadLine();
653 var data = Encoding.UTF8.GetBytes(line);
654 outStream.Write(data, 0, data.Length);
655 outStream.WriteByte((byte) '\n');
662 entry.Size = new FileInfo(tempFileName).Length;
663 entryFilename = tempFileName;
667 string newName = null;
669 if (rootPath != null)
671 if (entry.Name.StartsWith(rootPath))
673 newName = entry.Name.Substring(rootPath.Length + 1);
677 if (pathPrefix != null)
679 newName = (newName == null) ? string.Format("{0}/{1}", pathPrefix, entry.Name) : pathPrefix + "/" + newName;
684 entry.Name = newName;
687 tarOut.PutNextEntry(entry);
689 if (entry.IsDirectory)
693 var list = entry.GetDirectoryEntries();
694 for (var i = 0; i < list.Length; ++i)
696 InternalWriteEntry(list[i], recurse);
702 using (Stream inputStream = File.OpenRead(entryFilename))
704 var localBuffer = new byte[32*1024];
707 var numRead = inputStream.Read(localBuffer, 0, localBuffer.Length);
714 tarOut.Write(localBuffer, 0, numRead);
718 if (!string.IsNullOrEmpty(tempFileName))
720 File.Delete(tempFileName);
728 /// Releases the unmanaged resources used by the FileStream and optionally releases the managed resources.
730 /// <param name="disposing">true to release both managed and unmanaged resources;
731 /// false to release only unmanaged resources.</param>
732 protected virtual void Dispose(bool disposing)
754 /// Closes the archive and releases any associated resources.
756 public virtual void Close()
759 GC.SuppressFinalize(this);
763 /// Ensures that resources are freed and other cleanup operations are performed
764 /// when the garbage collector reclaims the <see cref="TarArchive"/>.
771 private static void EnsureDirectoryExists(string directoryName)
773 if (!Directory.Exists(directoryName))
777 Directory.CreateDirectory(directoryName);
781 throw new TarException("Exception creating directory '" + directoryName + "', " + e.Message, e);
786 // TODO: TarArchive - Is there a better way to test for a text file?
787 // It no longer reads entire files into memory but is still a weak test!
788 // This assumes that byte values 0-7, 14-31 or 255 are binary
789 // and that all non text files contain one of these values
790 private static bool IsBinary(string filename)
792 using (var fs = File.OpenRead(filename))
794 var sampleSize = Math.Min(4096, (int) fs.Length);
795 var content = new byte[sampleSize];
797 var bytesRead = fs.Read(content, 0, sampleSize);
799 for (var i = 0; i < bytesRead; ++i)
802 if ((b < 8) || ((b > 13) && (b < 32)) || (b == 255))
811 #region Instance Fields
813 private readonly TarInputStream tarIn;
814 private readonly TarOutputStream tarOut;
815 private bool _applyUserInfoOverrides;
816 private bool _asciiTranslate;
818 private int _groupId;
819 private string _groupName = string.Empty;
821 private bool isDisposed;
822 private bool _keepOldFiles;
823 private string pathPrefix;
824 private string rootPath;
826 private string _userName = string.Empty;
833 /// Constructor for a default <see cref="TarArchive"/>.
835 protected TarArchive()
840 /// Initalise a TarArchive for input.
842 /// <param name="stream">The <see cref="TarInputStream"/> to use for input.</param>
843 protected TarArchive(TarInputStream stream)
847 throw new ArgumentNullException("stream");
854 /// Initialise a TarArchive for output.
856 /// <param name="stream">The <see cref="TarOutputStream"/> to use for output.</param>
857 protected TarArchive(TarOutputStream stream)
861 throw new ArgumentNullException("stream");
869 #region Static factory methods
872 /// The InputStream based constructors create a TarArchive for the
873 /// purposes of extracting or listing a tar archive. Thus, use
874 /// these constructors when you wish to extract files from or list
875 /// the contents of an existing tar archive.
877 /// <param name="inputStream">The stream to retrieve archive data from.</param>
878 /// <returns>Returns a new <see cref="TarArchive"/> suitable for reading from.</returns>
879 public static TarArchive CreateInputTarArchive(Stream inputStream)
881 if (inputStream == null)
883 throw new ArgumentNullException("inputStream");
886 return CreateInputTarArchive(inputStream, TarBuffer.DefaultBlockFactor);
890 /// Create TarArchive for reading setting block factor
892 /// <param name="inputStream">Stream for tar archive contents</param>
893 /// <param name="blockFactor">The blocking factor to apply</param>
894 /// <returns>Returns a <see cref="TarArchive"/> suitable for reading.</returns>
895 public static TarArchive CreateInputTarArchive(Stream inputStream, int blockFactor)
897 if (inputStream == null)
899 throw new ArgumentNullException("inputStream");
902 return new TarArchive(new TarInputStream(inputStream, blockFactor));
906 /// Create a TarArchive for writing to, using the default blocking factor
908 /// <param name="outputStream">The <see cref="Stream"/> to write to</param>
909 /// <returns>Returns a <see cref="TarArchive"/> suitable for writing.</returns>
910 public static TarArchive CreateOutputTarArchive(Stream outputStream)
912 if (outputStream == null)
914 throw new ArgumentNullException("outputStream");
917 return CreateOutputTarArchive(outputStream, TarBuffer.DefaultBlockFactor);
921 /// Create a TarArchive for writing to
923 /// <param name="outputStream">The stream to write to</param>
924 /// <param name="blockFactor">The blocking factor to use for buffering.</param>
925 /// <returns>Returns a <see cref="TarArchive"/> suitable for writing.</returns>
926 public static TarArchive CreateOutputTarArchive(Stream outputStream, int blockFactor)
928 if (outputStream == null)
930 throw new ArgumentNullException("outputStream");
933 return new TarArchive(new TarOutputStream(outputStream, blockFactor));
940 /* The original Java file had this header:
941 ** Authored by Timothy Gerard Endres
942 ** <mailto:time@gjt.org> <http://www.trustice.com>
944 ** This work has been placed into the public domain.
945 ** You may use this work in any way and for any purpose you wish.
947 ** THIS SOFTWARE IS PROVIDED AS-IS WITHOUT WARRANTY OF ANY KIND,
948 ** NOT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY. THE AUTHOR
949 ** OF THIS SOFTWARE, ASSUMES _NO_ RESPONSIBILITY FOR ANY
950 ** CONSEQUENCE RESULTING FROM THE USE, MODIFICATION, OR
951 ** REDISTRIBUTION OF THIS SOFTWARE.