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