2 // Copyright (C) 2001 Mike Krueger
\r
4 // This program is free software; you can redistribute it and/or
\r
5 // modify it under the terms of the GNU General Public License
\r
6 // as published by the Free Software Foundation; either version 2
\r
7 // of the License, or (at your option) any later version.
\r
9 // This program is distributed in the hope that it will be useful,
\r
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
\r
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
\r
12 // GNU General Public License for more details.
\r
14 // You should have received a copy of the GNU General Public License
\r
15 // along with this program; if not, write to the Free Software
\r
16 // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
\r
18 // Linking this library statically or dynamically with other modules is
\r
19 // making a combined work based on this library. Thus, the terms and
\r
20 // conditions of the GNU General Public License cover the whole
\r
23 // As a special exception, the copyright holders of this library give you
\r
24 // permission to link this library with independent modules to produce an
\r
25 // executable, regardless of the license terms of these independent
\r
26 // modules, and to copy and distribute the resulting executable under
\r
27 // terms of your choice, provided that you also meet, for each linked
\r
28 // independent module, the terms and conditions of the license of that
\r
29 // module. An independent module is a module which is not derived from
\r
30 // or based on this library. If you modify this library, you may extend
\r
31 // this exception to your version of the library, but you are not
\r
32 // obligated to do so. If you do not wish to do so, delete this
\r
33 // exception statement from your version.
\r
39 namespace ICSharpCode.SharpZipLib.Tar {
\r
41 public delegate void ProgressMessageHandler(TarArchive archive, TarEntry entry, string message);
\r
44 /// The TarArchive class implements the concept of a
\r
45 /// tar archive. A tar archive is a series of entries, each of
\r
46 /// which represents a file system object. Each entry in
\r
47 /// the archive consists of a header block. Directory entries
\r
48 /// consist only of the header block, and are followed by entries
\r
49 /// for the directory's contents. File entries consist of a
\r
50 /// header followed by the number of blocks needed to
\r
51 /// contain the file's contents. All entries are written on
\r
52 /// block boundaries. Blocks are 512 bytes long.
\r
54 /// TarArchives are instantiated in either read or write mode,
\r
55 /// based upon whether they are instantiated with an InputStream
\r
56 /// or an OutputStream. Once instantiated TarArchives read/write
\r
57 /// mode can not be changed.
\r
59 /// There is currently no support for random access to tar archives.
\r
60 /// However, it seems that subclassing TarArchive, and using the
\r
61 /// TarBuffer.getCurrentRecordNum() and TarBuffer.getCurrentBlockNum()
\r
62 /// methods, this would be rather trvial.
\r
64 public class TarArchive
\r
69 bool asciiTranslate;
\r
82 TarInputStream tarIn;
\r
83 TarOutputStream tarOut;
\r
85 public event ProgressMessageHandler ProgressMessageEvent;
\r
87 protected virtual void OnProgressMessageEvent(TarEntry entry, string message)
\r
89 if (ProgressMessageEvent != null) {
\r
90 ProgressMessageEvent(this, entry, message);
\r
94 protected TarArchive()
\r
99 /// The InputStream based constructors create a TarArchive for the
\r
100 /// purposes of e'x'tracting or lis't'ing a tar archive. Thus, use
\r
101 /// these constructors when you wish to extract files from or list
\r
102 /// the contents of an existing tar archive.
\r
104 public static TarArchive CreateInputTarArchive(Stream inputStream)
\r
106 return CreateInputTarArchive(inputStream, TarBuffer.DefaultBlockFactor);
\r
109 public static TarArchive CreateInputTarArchive(Stream inputStream, int blockFactor)
\r
111 TarArchive archive = new TarArchive();
\r
112 archive.tarIn = new TarInputStream(inputStream, blockFactor);
\r
113 archive.Initialize(blockFactor * TarBuffer.BlockSize);
\r
118 /// The OutputStream based constructors create a TarArchive for the
\r
119 /// purposes of 'c'reating a tar archive. Thus, use these constructors
\r
120 /// when you wish to create a new tar archive and write files into it.
\r
122 public static TarArchive CreateOutputTarArchive(Stream outputStream)
\r
124 return CreateOutputTarArchive(outputStream, TarBuffer.DefaultBlockFactor);
\r
127 public static TarArchive CreateOutputTarArchive(Stream outputStream, int blockFactor)
\r
129 TarArchive archive = new TarArchive();
\r
130 archive.tarOut = new TarOutputStream(outputStream, blockFactor);
\r
131 archive.Initialize(blockFactor * TarBuffer.BlockSize);
\r
136 /// Common constructor initialization code.
\r
138 void Initialize(int recordSize)
\r
140 this.recordSize = recordSize;
\r
141 this.rootPath = null;
\r
142 this.pathPrefix = null;
\r
144 // this.tempPath = System.getProperty( "user.dir" );
\r
147 this.userName = String.Empty;
\r
149 this.groupName = String.Empty;
\r
151 this.debug = false;
\r
152 this.verbose = false;
\r
153 this.keepOldFiles = false;
\r
155 this.recordBuf = new byte[RecordSize];
\r
159 /// <summary> Set the debugging flag. </summary>
\r
161 /// <param name=debugF> The new debug setting. </param>
\r
163 public void SetDebug(bool debugF)
\r
165 this.debug = debugF;
\r
166 if (this.tarIn != null) {
\r
167 this.tarIn.SetDebug(debugF);
\r
169 if (this.tarOut != null) {
\r
170 this.tarOut.SetDebug(debugF);
\r
175 /// Get/Set the verbosity setting.
\r
177 public bool IsVerbose {
\r
187 /// Set the flag that determines whether existing files are
\r
188 /// kept, or overwritten during extraction.
\r
190 /// <param name="keepOldFiles">
\r
191 /// If true, do not overwrite existing files.
\r
193 public void SetKeepOldFiles(bool keepOldFiles)
\r
195 this.keepOldFiles = keepOldFiles;
\r
199 /// Set the ascii file translation flag. If ascii file translation
\r
200 /// is true, then the MIME file type will be consulted to determine
\r
201 /// if the file is of type 'text/*'. If the MIME type is not found,
\r
202 /// then the TransFileTyper is consulted if it is not null. If
\r
203 /// either of these two checks indicates the file is an ascii text
\r
204 /// file, it will be translated. The translation converts the local
\r
205 /// operating system's concept of line ends into the UNIX line end,
\r
206 /// '\n', which is the defacto standard for a TAR archive. This makes
\r
207 /// text files compatible with UNIX.
\r
209 /// <param name= "asciiTranslate">
\r
210 /// If true, translate ascii text files.
\r
212 public void SetAsciiTranslation(bool asciiTranslate)
\r
214 this.asciiTranslate = asciiTranslate;
\r
219 /// Set the object that will determine if a file is of type
\r
220 /// ascii text for translation purposes.
\r
222 /// <param name="transTyper">
\r
223 /// The new TransFileTyper object.
\r
225 public void SetTransFileTyper(TarTransFileTyper transTyper)
\r
227 this.transTyper = transTyper;
\r
232 /// Set user and group information that will be used to fill in the
\r
233 /// tar archive's entry headers. Since Java currently provides no means
\r
234 /// of determining a user name, user id, group name, or group id for
\r
235 /// a given File, TarArchive allows the programmer to specify values
\r
236 /// to be used in their place.
\r
238 /// <param name="userId">
\r
239 /// The user Id to use in the headers.
\r
241 /// <param name="userName">
\r
242 /// The user name to use in the headers.
\r
244 /// <param name="groupId">
\r
245 /// The group id to use in the headers.
\r
247 /// <param name="groupName">
\r
248 /// The group name to use in the headers.
\r
250 public void SetUserInfo(int userId, string userName, int groupId, string groupName)
\r
252 this.userId = userId;
\r
253 this.userName = userName;
\r
254 this.groupId = groupId;
\r
255 this.groupName = groupName;
\r
259 /// Get the user id being used for archive entry headers.
\r
262 /// The current user id.
\r
264 public int UserId {
\r
266 return this.userId;
\r
271 /// Get the user name being used for archive entry headers.
\r
274 /// The current user name.
\r
276 public string UserName {
\r
278 return this.userName;
\r
283 /// Get the group id being used for archive entry headers.
\r
286 /// The current group id.
\r
288 public int GroupId {
\r
290 return this.groupId;
\r
295 /// Get the group name being used for archive entry headers.
\r
298 /// The current group name.
\r
300 public string GroupName {
\r
302 return this.groupName;
\r
307 /// Get the archive's record size. Because of its history, tar
\r
308 /// supports the concept of buffered IO consisting of RECORDS of
\r
309 /// BLOCKS. This allowed tar to match the IO characteristics of
\r
310 /// the physical device being used. Of course, in the Java world,
\r
311 /// this makes no sense, WITH ONE EXCEPTION - archives are expected
\r
312 /// to be properly "blocked". Thus, all of the horrible TarBuffer
\r
313 /// support boils down to simply getting the "boundaries" correct.
\r
316 /// The record size this archive is using.
\r
318 public int RecordSize {
\r
320 if (this.tarIn != null) {
\r
321 return this.tarIn.GetRecordSize();
\r
323 else if (this.tarOut != null) {
\r
324 return this.tarOut.GetRecordSize();
\r
326 return TarBuffer.DefaultRecordSize;
\r
331 /// Close the archive. This simply calls the underlying
\r
332 /// tar stream's close() method.
\r
334 public void CloseArchive()
\r
336 if (this.tarIn != null) {
\r
337 this.tarIn.Close();
\r
339 else if (this.tarOut != null) {
\r
340 this.tarOut.Flush();
\r
341 this.tarOut.Close();
\r
346 /// Perform the "list" command and list the contents of the archive.
\r
348 /// NOTE That this method uses the progress display to actually list
\r
349 /// the conents. If the progress display is not set, nothing will be
\r
352 public void ListContents()
\r
355 TarEntry entry = this.tarIn.GetNextEntry();
\r
357 if (entry == null) {
\r
359 Console.Error.WriteLine("READ EOF BLOCK");
\r
363 OnProgressMessageEvent(entry, null);
\r
368 /// Perform the "extract" command and extract the contents of the archive.
\r
370 /// <param name="destDir">
\r
371 /// The destination directory into which to extract.
\r
373 public void ExtractContents(string destDir)
\r
376 TarEntry entry = this.tarIn.GetNextEntry();
\r
378 if (entry == null) {
\r
380 Console.Error.WriteLine("READ EOF BLOCK");
\r
385 this.ExtractEntry(destDir, entry);
\r
389 void EnsureDirectoryExists(string directoryName)
\r
391 if (!Directory.Exists(directoryName)) {
\r
393 Directory.CreateDirectory(directoryName);
\r
395 catch (Exception e) {
\r
396 throw new IOException("error making directory path '" + directoryName + "', " + e.Message);
\r
401 // TODO -jr- No longer reads entire file into memory but is still a weak test!
\r
402 bool IsBinary(string filename)
\r
404 FileStream fs = File.OpenRead(filename);
\r
406 int sampleSize = System.Math.Min(4096, (int)fs.Length);
\r
407 byte[] content = new byte[sampleSize];
\r
409 fs.Read(content, 0, sampleSize);
\r
412 // assume that ascii 0 or
\r
413 // ascii 255 are only found in non text files.
\r
414 // and that all non text files contain 0 and 255
\r
415 foreach (byte b in content) {
\r
416 if (b == 0 || b == 255) {
\r
425 /// Extract an entry from the archive. This method assumes that the
\r
426 /// tarIn stream has been properly set with a call to getNextEntry().
\r
428 /// <param name="destDir">
\r
429 /// The destination directory into which to extract.
\r
431 /// <param name="entry">
\r
432 /// The TarEntry returned by tarIn.getNextEntry().
\r
434 void ExtractEntry(string destDir, TarEntry entry)
\r
436 if (this.verbose) {
\r
437 OnProgressMessageEvent(entry, null);
\r
440 string name = entry.Name;
\r
441 name = name.Replace('/', Path.DirectorySeparatorChar);
\r
443 if (!destDir.EndsWith(Path.DirectorySeparatorChar.ToString())) {
\r
444 destDir += Path.DirectorySeparatorChar;
\r
447 string destFile = destDir + name;
\r
449 if (entry.IsDirectory) {
\r
450 EnsureDirectoryExists(destFile);
\r
453 string parentDirectory = Path.GetDirectoryName(destFile);
\r
454 EnsureDirectoryExists(parentDirectory);
\r
456 if (this.keepOldFiles && File.Exists(destFile)) {
\r
457 if (this.verbose) {
\r
458 OnProgressMessageEvent(entry, "Destination file already exists");
\r
462 bool asciiTrans = false;
\r
463 Stream outputStream = File.Create(destFile);
\r
464 if (this.asciiTranslate) {
\r
465 asciiTrans = !IsBinary(destFile);
\r
466 // original java sourcecode :
\r
467 // MimeType mime = null;
\r
468 // string contentType = null;
\r
470 // contentType = FileTypeMap.getDefaultFileTypeMap().getContentType( destFile );
\r
472 // mime = new MimeType(contentType);
\r
474 // if (mime.getPrimaryType().equalsIgnoreCase( "text" )) {
\r
475 // asciiTrans = true;
\r
476 // } else if ( this.transTyper != null ) {
\r
477 // if ( this.transTyper.isAsciiFile( entry.getName() ) ) {
\r
478 // asciiTrans = true;
\r
481 // } catch (MimeTypeParseException ex) {
\r
484 // if (this.debug) {
\r
485 // Console.Error.WriteLine(("EXTRACT TRANS? '" + asciiTrans + "' ContentType='" + contentType + "' PrimaryType='" + mime.getPrimaryType() + "'" );
\r
489 StreamWriter outw = null;
\r
491 outw = new StreamWriter(outputStream);
\r
494 byte[] rdbuf = new byte[32 * 1024];
\r
497 int numRead = this.tarIn.Read(rdbuf, 0, rdbuf.Length);
\r
499 if (numRead <= 0) {
\r
504 for (int off = 0, b = 0; b < numRead; ++b) {
\r
505 if (rdbuf[b] == 10) {
\r
506 string s = Encoding.ASCII.GetString(rdbuf, off, (b - off));
\r
513 outputStream.Write(rdbuf, 0, numRead);
\r
521 outputStream.Close();
\r
528 /// Write an entry to the archive. This method will call the putNextEntry
\r
529 /// and then write the contents of the entry, and finally call closeEntry()()
\r
530 /// for entries that are files. For directories, it will call putNextEntry(),
\r
531 /// and then, if the recurse flag is true, process each entry that is a
\r
532 /// child of the directory.
\r
534 /// <param name="entry">
\r
535 /// The TarEntry representing the entry to write to the archive.
\r
537 /// <param name="recurse">
\r
538 /// If true, process the children of directory entries.
\r
540 public void WriteEntry(TarEntry entry, bool recurse)
\r
542 bool asciiTrans = false;
\r
544 string tempFileName = null;
\r
545 string eFile = entry.File;
\r
547 // Work on a copy of the entry so we can manipulate it.
\r
548 // Note that we must distinguish how the entry was constructed.
\r
550 if (eFile == null || eFile.Length == 0) {
\r
551 entry = TarEntry.CreateTarEntry(entry.Name);
\r
555 // The user may have explicitly set the entry's name to
\r
556 // something other than the file's path, so we must save
\r
557 // and restore it. This should work even when the name
\r
558 // was set from the File's name.
\r
560 string saveName = entry.Name;
\r
561 entry = TarEntry.CreateEntryFromFile(eFile);
\r
562 entry.Name = saveName;
\r
565 if (this.verbose) {
\r
566 OnProgressMessageEvent(entry, null);
\r
569 if (this.asciiTranslate && !entry.IsDirectory) {
\r
570 asciiTrans = !IsBinary(eFile);
\r
572 // original java source :
\r
573 // MimeType mime = null;
\r
574 // string contentType = null;
\r
577 // contentType = FileTypeMap.getDefaultFileTypeMap(). getContentType( eFile );
\r
579 // mime = new MimeType( contentType );
\r
581 // if ( mime.getPrimaryType().
\r
582 // equalsIgnoreCase( "text" ) )
\r
584 // asciiTrans = true;
\r
586 // else if ( this.transTyper != null )
\r
588 // if ( this.transTyper.isAsciiFile( eFile ) )
\r
590 // asciiTrans = true;
\r
593 // } catch ( MimeTypeParseException ex )
\r
595 // // IGNORE THIS ERROR...
\r
598 // if (this.debug) {
\r
599 // Console.Error.WriteLine("CREATE TRANS? '" + asciiTrans + "' ContentType='" + contentType + "' PrimaryType='" + mime.getPrimaryType()+ "'" );
\r
603 tempFileName = Path.GetTempFileName();
\r
605 StreamReader inStream = File.OpenText(eFile);
\r
606 Stream outStream = new BufferedStream(File.Create(tempFileName));
\r
609 string line = inStream.ReadLine();
\r
610 if (line == null) {
\r
613 byte[] data = Encoding.ASCII.GetBytes(line);
\r
614 outStream.Write(data, 0, data.Length);
\r
615 outStream.WriteByte((byte)'\n');
\r
623 entry.Size = new FileInfo(tempFileName).Length;
\r
625 eFile = tempFileName;
\r
629 string newName = null;
\r
631 if (this.rootPath != null) {
\r
632 if (entry.Name.StartsWith(this.rootPath)) {
\r
633 newName = entry.Name.Substring(this.rootPath.Length + 1 );
\r
637 if (this.pathPrefix != null) {
\r
638 newName = (newName == null) ? this.pathPrefix + "/" + entry.Name : this.pathPrefix + "/" + newName;
\r
641 if (newName != null) {
\r
642 entry.Name = newName;
\r
645 this.tarOut.PutNextEntry(entry);
\r
647 if (entry.IsDirectory) {
\r
649 TarEntry[] list = entry.GetDirectoryEntries();
\r
650 for (int i = 0; i < list.Length; ++i) {
\r
651 this.WriteEntry(list[i], recurse);
\r
656 Stream inputStream = File.OpenRead(eFile);
\r
657 int numWritten = 0;
\r
658 byte[] eBuf = new byte[32 * 1024];
\r
660 int numRead = inputStream.Read(eBuf, 0, eBuf.Length);
\r
666 this.tarOut.Write(eBuf, 0, numRead);
\r
667 numWritten += numRead;
\r
670 // Console.WriteLine("written " + numWritten + " bytes");
\r
672 inputStream.Close();
\r
674 if (tempFileName != null && tempFileName.Length > 0) {
\r
675 File.Delete(tempFileName);
\r
678 this.tarOut.CloseEntry();
\r
683 /* The original Java file had this header:
\r
684 ** Authored by Timothy Gerard Endres
\r
685 ** <mailto:time@gjt.org> <http://www.trustice.com>
\r
687 ** This work has been placed into the public domain.
\r
688 ** You may use this work in any way and for any purpose you wish.
\r
690 ** THIS SOFTWARE IS PROVIDED AS-IS WITHOUT WARRANTY OF ANY KIND,
\r
691 ** NOT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY. THE AUTHOR
\r
692 ** OF THIS SOFTWARE, ASSUMES _NO_ RESPONSIBILITY FOR ANY
\r
693 ** CONSEQUENCE RESULTING FROM THE USE, MODIFICATION, OR
\r
694 ** REDISTRIBUTION OF THIS SOFTWARE.
\r