1 // TarArchive.cs
2 //
3 // Copyright (C) 2001 Mike Krueger
4 //
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.
9 //
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.
14 //
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.
18 //
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
22 // combination.
23 //
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.
35 
36 using System;
37 using System.IO;
38 using System.Text;
39 
40 namespace ICSharpCode.SharpZipLib.Tar {
41 	/// <summary>
42 	/// Used to advise clients of 'events' while processing archives
43 	/// </summary>
44 	[System.ObsoleteAttribute("This assembly has been deprecated. Please use https://www.nuget.org/packages/SharpZipLib/ instead.")]
ProgressMessageHandler(TarArchive archive, TarEntry entry, string message)45 	public delegate void ProgressMessageHandler(TarArchive archive, TarEntry entry, string message);
46 
47 	/// <summary>
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.
57 	///
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.
62 	///
63 	/// There is currently no support for random access to tar archives.
64 	/// However, it seems that subclassing TarArchive, and using the
65 	/// TarBuffer.getCurrentRecordNum() and TarBuffer.getCurrentBlockNum()
66 	/// methods, this would be rather trvial.
67 	/// </summary>
68 	[System.ObsoleteAttribute("This assembly has been deprecated. Please use https://www.nuget.org/packages/SharpZipLib/ instead.")]
69 	public class TarArchive
70 	{
71 		bool keepOldFiles;
72 		bool asciiTranslate;
73 
74 		int    userId;
75 		string userName;
76 		int    groupId;
77 		string groupName;
78 
79 		string rootPath;
80 		string pathPrefix;
81 
82 		int    recordSize;
83 		byte[] recordBuf;
84 
85 		TarInputStream  tarIn;
86 		TarOutputStream tarOut;
87 
88 		/// <summary>
89 		/// Client hook allowing detailed information to be reported during processing
90 		/// </summary>
91 		public event ProgressMessageHandler ProgressMessageEvent;
92 
93 		/// <summary>
94 		/// Raises the ProgressMessage event
95 		/// </summary>
96 		/// <param name="entry">TarEntry for this event</param>
97 		/// <param name="message">message for this event.  Null is no message</param>
OnProgressMessageEvent(TarEntry entry, string message)98 		protected virtual void OnProgressMessageEvent(TarEntry entry, string message)
99 		{
100 			if (ProgressMessageEvent != null) {
101 				ProgressMessageEvent(this, entry, message);
102 			}
103 		}
104 
105 		/// <summary>
106 		/// Constructor for a TarArchive.
107 		/// </summary>
TarArchive()108 		protected TarArchive()
109 		{
110 		}
111 
112 		/// <summary>
113 		/// The InputStream based constructors create a TarArchive for the
114 		/// purposes of extracting or listing a tar archive. Thus, use
115 		/// these constructors when you wish to extract files from or list
116 		/// the contents of an existing tar archive.
117 		/// </summary>
CreateInputTarArchive(Stream inputStream)118 		public static TarArchive CreateInputTarArchive(Stream inputStream)
119 		{
120 			return CreateInputTarArchive(inputStream, TarBuffer.DefaultBlockFactor);
121 		}
122 
123 		/// <summary>
124 		/// Create TarArchive for reading setting block factor
125 		/// </summary>
126 		/// <param name="inputStream">Stream for tar archive contents</param>
127 		/// <param name="blockFactor">The blocking factor to apply</param>
128 		/// <returns>
129 		/// TarArchive
130 		/// </returns>
CreateInputTarArchive(Stream inputStream, int blockFactor)131 		public static TarArchive CreateInputTarArchive(Stream inputStream, int blockFactor)
132 		{
133 			TarArchive archive = new TarArchive();
134 			archive.tarIn = new TarInputStream(inputStream, blockFactor);
135 			archive.Initialize(blockFactor * TarBuffer.BlockSize);
136 			return archive;
137 		}
138 
139 		/// <summary>
140 		/// Create a TarArchive for writing to, using the default blocking factor
141 		/// </summary>
142 		/// <param name="outputStream">Stream to write to</param>
CreateOutputTarArchive(Stream outputStream)143 		public static TarArchive CreateOutputTarArchive(Stream outputStream)
144 		{
145 			return CreateOutputTarArchive(outputStream, TarBuffer.DefaultBlockFactor);
146 		}
147 
148 		/// <summary>
149 		/// Create a TarArchive for writing to
150 		/// </summary>
151 		/// <param name="outputStream">The stream to write to</param>
152 		/// <param name="blockFactor">The blocking factor to use for buffering.</param>
CreateOutputTarArchive(Stream outputStream, int blockFactor)153 		public static TarArchive CreateOutputTarArchive(Stream outputStream, int blockFactor)
154 		{
155 			TarArchive archive = new TarArchive();
156 			archive.tarOut = new TarOutputStream(outputStream, blockFactor);
157 			archive.Initialize(blockFactor * TarBuffer.BlockSize);
158 			return archive;
159 		}
160 
161 		/// <summary>
162 		/// Common constructor initialization code.
163 		/// </summary>
Initialize(int recordSize)164 		void Initialize(int recordSize)
165 		{
166 			this.recordSize = recordSize;
167 			this.rootPath   = null;
168 			this.pathPrefix = null;
169 
170 			this.userId    = 0;
171 			this.userName  = String.Empty;
172 			this.groupId   = 0;
173 			this.groupName = String.Empty;
174 
175 			this.keepOldFiles    = false;
176 
177 			this.recordBuf = new byte[RecordSize];
178 		}
179 
180 		/// <summary>
181 		/// Set the flag that determines whether existing files are
182 		/// kept, or overwritten during extraction.
183 		/// </summary>
184 		/// <param name="keepOldFiles">
185 		/// If true, do not overwrite existing files.
186 		/// </param>
SetKeepOldFiles(bool keepOldFiles)187 		public void SetKeepOldFiles(bool keepOldFiles)
188 		{
189 			this.keepOldFiles = keepOldFiles;
190 		}
191 
192 		/// <summary>
193 		/// Set the ascii file translation flag. If ascii file translation
194 		/// is true, then the file is checked to see if it a binary file or not.
195 		/// If the flag is true and the test indicates it is ascii text
196 		/// file, it will be translated. The translation converts the local
197 		/// operating system's concept of line ends into the UNIX line end,
198 		/// '\n', which is the defacto standard for a TAR archive. This makes
199 		/// text files compatible with UNIX.
200 		/// </summary>
201 		/// <param name= "asciiTranslate">
202 		/// If true, translate ascii text files.
203 		/// </param>
SetAsciiTranslation(bool asciiTranslate)204 		public void SetAsciiTranslation(bool asciiTranslate)
205 		{
206 			this.asciiTranslate = asciiTranslate;
207 		}
208 
209 		/// <summary>
210 		/// PathPrefix is added to entry names as they are written if the value is not null.
211 		/// A slash character is appended after PathPrefix
212 		/// </summary>
213 		public string PathPrefix
214 		{
215 			get { return pathPrefix; }
216 			set { pathPrefix = value; }
217 
218 		}
219 
220 		/// <summary>
221 		/// RootPath is removed from entry names if it is found at the
222 		/// beginning of the name.
223 		/// </summary>
224 		public string RootPath
225 		{
226 			get { return rootPath; }
227 			set { rootPath = value; }
228 		}
229 
230 		/// <summary>
231 		/// Set user and group information that will be used to fill in the
232 		/// tar archive's entry headers. This information based on that available
233 		/// for the linux operating system, which is not always available on other
234 		/// operating systems.  TarArchive allows the programmer to specify values
235 		/// to be used in their place.
236 		/// </summary>
237 		/// <param name="userId">
238 		/// The user id to use in the headers.
239 		/// </param>
240 		/// <param name="userName">
241 		/// The user name to use in the headers.
242 		/// </param>
243 		/// <param name="groupId">
244 		/// The group id to use in the headers.
245 		/// </param>
246 		/// <param name="groupName">
247 		/// The group name to use in the headers.
248 		/// </param>
SetUserInfo(int userId, string userName, int groupId, string groupName)249 		public void SetUserInfo(int userId, string userName, int groupId, string groupName)
250 		{
251 			this.userId    = userId;
252 			this.userName  = userName;
253 			this.groupId   = groupId;
254 			this.groupName = groupName;
255 			applyUserInfoOverrides = true;
256 		}
257 
258 		bool applyUserInfoOverrides = false;
259 
260 		/// <summary>
261 		/// Get or set a value indicating if overrides defined by <see cref="SetUserInfo">SetUserInfo</see> should be applied.
262 		/// </summary>
263 		/// <remarks>If overrides are not applied then the values as set in each header will be used.</remarks>
264 		public bool ApplyUserInfoOverrides
265 		{
266 			get { return applyUserInfoOverrides; }
267 			set { applyUserInfoOverrides = value; }
268 		}
269 
270 		/// <summary>
271 		/// Get the archive user id.
272 		/// See <see cref="ApplyUserInfoOverrides">ApplyUserInfoOverrides</see> for detail
273 		/// on how to allow setting values on a per entry basis.
274 		/// </summary>
275 		/// <returns>
276 		/// The current user id.
277 		/// </returns>
278 		public int UserId {
279 			get {
280 				return this.userId;
281 			}
282 		}
283 
284 		/// <summary>
285 		/// Get the archive user name.
286 		/// See <see cref="ApplyUserInfoOverrides">ApplyUserInfoOverrides</see> for detail
287 		/// on how to allow setting values on a per entry basis.
288 		/// </summary>
289 		/// <returns>
290 		/// The current user name.
291 		/// </returns>
292 		public string UserName {
293 			get {
294 				return this.userName;
295 			}
296 		}
297 
298 		/// <summary>
299 		/// Get the archive group id.
300 		/// See <see cref="ApplyUserInfoOverrides">ApplyUserInfoOverrides</see> for detail
301 		/// on how to allow setting values on a per entry basis.
302 		/// </summary>
303 		/// <returns>
304 		/// The current group id.
305 		/// </returns>
306 		public int GroupId {
307 			get {
308 				return this.groupId;
309 			}
310 		}
311 
312 		/// <summary>
313 		/// Get the archive group name.
314 		/// See <see cref="ApplyUserInfoOverrides">ApplyUserInfoOverrides</see> for detail
315 		/// on how to allow setting values on a per entry basis.
316 		/// </summary>
317 		/// <returns>
318 		/// The current group name.
319 		/// </returns>
320 		public string GroupName {
321 			get {
322 				return this.groupName;
323 			}
324 		}
325 
326 		/// <summary>
327 		/// Get the archive's record size. Because of its history, tar
328 		/// supports the concept of buffered IO consisting of RECORDS of
329 		/// BLOCKS. This allowed tar to match the IO characteristics of
330 		/// the physical device being used. Of course, in the C# world,
331 		/// this makes no sense, WITH ONE EXCEPTION - archives are expected
332 		/// to be properly "blocked". Thus, all of the horrible TarBuffer
333 		/// support boils down to simply getting the "boundaries" correct.
334 		/// </summary>
335 		/// <returns>
336 		/// The record size this archive is using.
337 		/// </returns>
338 		public int RecordSize {
339 			get {
340 				if (this.tarIn != null) {
341 					return this.tarIn.GetRecordSize();
342 				} else if (this.tarOut != null) {
343 					return this.tarOut.GetRecordSize();
344 				}
345 				return TarBuffer.DefaultRecordSize;
346 			}
347 		}
348 
349 		/// <summary>
350 		/// Close the archive. This simply calls the underlying
351 		/// tar stream's close() method.
352 		/// </summary>
CloseArchive()353 		public void CloseArchive()
354 		{
355 			if (this.tarIn != null) {
356 				this.tarIn.Close();
357 			} else if (this.tarOut != null) {
358 				this.tarOut.Flush();
359 				this.tarOut.Close();
360 			}
361 		}
362 
363 		/// <summary>
364 		/// Perform the "list" command for the archive contents.
365 		///
366 		/// NOTE That this method uses the <see cref="ProgressMessageEvent"> progress event</see> to actually list
367 		/// the contents. If the progress display event is not set, nothing will be listed!
368 		/// </summary>
ListContents()369 		public void ListContents()
370 		{
371 			while (true) {
372 				TarEntry entry = this.tarIn.GetNextEntry();
373 
374 				if (entry == null) {
375 					break;
376 				}
377 				OnProgressMessageEvent(entry, null);
378 			}
379 		}
380 
381 		/// <summary>
382 		/// Perform the "extract" command and extract the contents of the archive.
383 		/// </summary>
384 		/// <param name="destDir">
385 		/// The destination directory into which to extract.
386 		/// </param>
ExtractContents(string destDir)387 		public void ExtractContents(string destDir)
388 		{
389 			while (true) {
390 				TarEntry entry = this.tarIn.GetNextEntry();
391 
392 				if (entry == null) {
393 					break;
394 				}
395 
396 				this.ExtractEntry(destDir, entry);
397 			}
398 		}
399 
EnsureDirectoryExists(string directoryName)400 		void EnsureDirectoryExists(string directoryName)
401 		{
402 			if (!Directory.Exists(directoryName)) {
403 				try {
404 					Directory.CreateDirectory(directoryName);
405 				}
406 				catch (Exception e) {
407 					throw new TarException("Exception creating directory '" + directoryName + "', " + e.Message);
408 				}
409 			}
410 		}
411 
412 		// TODO: Is there a better way to test for a text file?
413 		// It no longer reads entire files into memory but is still a weak test!
414 		// assumes that ascii 0-7, 14-31 or 255 are binary
415 		// and that all non text files contain one of these values
IsBinary(string filename)416 		bool IsBinary(string filename)
417 		{
418 			using (FileStream fs = File.OpenRead(filename))
419 			{
420 				int sampleSize = System.Math.Min(4096, (int)fs.Length);
421 				byte[] content = new byte[sampleSize];
422 
423 				int bytesRead = fs.Read(content, 0, sampleSize);
424 
425 				for (int i = 0; i < bytesRead; ++i) {
426 					byte b = content[i];
427 					if (b < 8 || (b > 13 && b < 32) || b == 255) {
428 						return true;
429 					}
430 				}
431 			}
432 			return false;
433 		}
434 
435 		/// <summary>
436 		/// Extract an entry from the archive. This method assumes that the
437 		/// tarIn stream has been properly set with a call to getNextEntry().
438 		/// </summary>
439 		/// <param name="destDir">
440 		/// The destination directory into which to extract.
441 		/// </param>
442 		/// <param name="entry">
443 		/// The TarEntry returned by tarIn.getNextEntry().
444 		/// </param>
ExtractEntry(string destDir, TarEntry entry)445 		void ExtractEntry(string destDir, TarEntry entry)
446 		{
447 			OnProgressMessageEvent(entry, null);
448 
449 			string name = entry.Name;
450 
451 			if (Path.IsPathRooted(name) == true) {
452 				// NOTE:
453 				// for UNC names...  \\machine\share\zoom\beet.txt gives \zoom\beet.txt
454 				name = name.Substring(Path.GetPathRoot(name).Length);
455 			}
456 
457 			name = name.Replace('/', Path.DirectorySeparatorChar);
458 
459 			string destFile = Path.Combine(destDir, name);
460 
461 			if (entry.IsDirectory) {
462 				EnsureDirectoryExists(destFile);
463 			} else {
464 				string parentDirectory = Path.GetDirectoryName(destFile);
465 				EnsureDirectoryExists(parentDirectory);
466 
467 				bool process = true;
468 				FileInfo fileInfo = new FileInfo(destFile);
469 				if (fileInfo.Exists) {
470 					if (this.keepOldFiles) {
471 						OnProgressMessageEvent(entry, "Destination file already exists");
472 						process = false;
473 					} else if ((fileInfo.Attributes & FileAttributes.ReadOnly) != 0) {
474 						OnProgressMessageEvent(entry, "Destination file already exists, and is read-only");
475 						process = false;
476 					}
477 				}
478 
479 				if (process) {
480 					bool asciiTrans = false;
481 
482 					Stream outputStream = File.Create(destFile);
483 					if (this.asciiTranslate) {
484 						asciiTrans = !IsBinary(destFile);
485 					}
486 
487 					StreamWriter outw = null;
488 					if (asciiTrans) {
489 						outw = new StreamWriter(outputStream);
490 					}
491 
492 					byte[] rdbuf = new byte[32 * 1024];
493 
494 					while (true) {
495 						int numRead = this.tarIn.Read(rdbuf, 0, rdbuf.Length);
496 
497 						if (numRead <= 0) {
498 							break;
499 						}
500 
501 						if (asciiTrans) {
502 							for (int off = 0, b = 0; b < numRead; ++b) {
503 								if (rdbuf[b] == 10) {
504 									string s = Encoding.ASCII.GetString(rdbuf, off, (b - off));
505 									outw.WriteLine(s);
506 									off = b + 1;
507 								}
508 							}
509 						} else {
510 							outputStream.Write(rdbuf, 0, numRead);
511 						}
512 					}
513 
514 					if (asciiTrans) {
515 						outw.Close();
516 					} else {
517 						outputStream.Close();
518 					}
519 				}
520 			}
521 		}
522 
523 		/// <summary>
524 		/// Write an entry to the archive. This method will call the putNextEntry
525 		/// and then write the contents of the entry, and finally call closeEntry()
526 		/// for entries that are files. For directories, it will call putNextEntry(),
527 		/// and then, if the recurse flag is true, process each entry that is a
528 		/// child of the directory.
529 		/// </summary>
530 		/// <param name="sourceEntry">
531 		/// The TarEntry representing the entry to write to the archive.
532 		/// </param>
533 		/// <param name="recurse">
534 		/// If true, process the children of directory entries.
535 		/// </param>
WriteEntry(TarEntry sourceEntry, bool recurse)536 		public void WriteEntry(TarEntry sourceEntry, bool recurse)
537 		{
538 			try
539 			{
540 				if ( recurse ) {
541 					TarHeader.SetValueDefaults(sourceEntry.UserId, sourceEntry.UserName,
542 					                           sourceEntry.GroupId, sourceEntry.GroupName);
543 				}
544 				InternalWriteEntry(sourceEntry, recurse);
545 			}
546 			finally
547 			{
548 				if ( recurse ) {
549 					TarHeader.RestoreSetValues();
550 				}
551 			}
552 		}
553 
554 		/// <summary>
555 		/// Write an entry to the archive. This method will call the putNextEntry
556 		/// and then write the contents of the entry, and finally call closeEntry()
557 		/// for entries that are files. For directories, it will call putNextEntry(),
558 		/// and then, if the recurse flag is true, process each entry that is a
559 		/// child of the directory.
560 		/// </summary>
561 		/// <param name="sourceEntry">
562 		/// The TarEntry representing the entry to write to the archive.
563 		/// </param>
564 		/// <param name="recurse">
565 		/// If true, process the children of directory entries.
566 		/// </param>
InternalWriteEntry(TarEntry sourceEntry, bool recurse)567 		void InternalWriteEntry(TarEntry sourceEntry, bool recurse)
568 		{
569 			bool asciiTrans = false;
570 
571 			string tempFileName = null;
572 			string entryFilename   = sourceEntry.File;
573 
574 			TarEntry entry = (TarEntry)sourceEntry.Clone();
575 
576 			if ( applyUserInfoOverrides ) {
577 				entry.GroupId = groupId;
578 				entry.GroupName = groupName;
579 				entry.UserId = userId;
580 				entry.UserName = userName;
581 			}
582 
583 			OnProgressMessageEvent(entry, null);
584 
585 			if (this.asciiTranslate && !entry.IsDirectory) {
586 				asciiTrans = !IsBinary(entryFilename);
587 
588 				if (asciiTrans) {
589 					tempFileName = Path.GetTempFileName();
590 
591 					StreamReader inStream  = File.OpenText(entryFilename);
592 					Stream       outStream = File.Create(tempFileName);
593 
594 					while (true) {
595 						string line = inStream.ReadLine();
596 						if (line == null) {
597 							break;
598 						}
599 						byte[] data = Encoding.ASCII.GetBytes(line);
600 						outStream.Write(data, 0, data.Length);
601 						outStream.WriteByte((byte)'\n');
602 					}
603 
604 					inStream.Close();
605 
606 					outStream.Flush();
607 					outStream.Close();
608 
609 					entry.Size = new FileInfo(tempFileName).Length;
610 
611 					entryFilename = tempFileName;
612 				}
613 			}
614 
615 			string newName = null;
616 
617 			if (this.rootPath != null) {
618 				if (entry.Name.StartsWith(this.rootPath)) {
619 					newName = entry.Name.Substring(this.rootPath.Length + 1 );
620 				}
621 			}
622 
623 			if (this.pathPrefix != null) {
624 				newName = (newName == null) ? this.pathPrefix + "/" + entry.Name : this.pathPrefix + "/" + newName;
625 			}
626 
627 			if (newName != null) {
628 				entry.Name = newName;
629 			}
630 
631 			this.tarOut.PutNextEntry(entry);
632 
633 			if (entry.IsDirectory) {
634 				if (recurse) {
635 					TarEntry[] list = entry.GetDirectoryEntries();
636 					for (int i = 0; i < list.Length; ++i) {
637 						InternalWriteEntry(list[i], recurse);
638 					}
639 				}
640 			} else {
641 				Stream inputStream = File.OpenRead(entryFilename);
642 				int numWritten = 0;
643 				byte[] eBuf = new byte[32 * 1024];
644 				while (true) {
645 					int numRead = inputStream.Read(eBuf, 0, eBuf.Length);
646 
647 					if (numRead <=0) {
648 						break;
649 					}
650 
651 					this.tarOut.Write(eBuf, 0, numRead);
652 					numWritten +=  numRead;
653 				}
654 
655 				inputStream.Close();
656 
657 				if (tempFileName != null && tempFileName.Length > 0) {
658 					File.Delete(tempFileName);
659 				}
660 
661 				this.tarOut.CloseEntry();
662 			}
663 		}
664 	}
665 }
666 
667 
668 /* The original Java file had this header:
669 	** Authored by Timothy Gerard Endres
670 	** <mailto:time@gjt.org>  <http://www.trustice.com>
671 	**
672 	** This work has been placed into the public domain.
673 	** You may use this work in any way and for any purpose you wish.
674 	**
675 	** THIS SOFTWARE IS PROVIDED AS-IS WITHOUT WARRANTY OF ANY KIND,
676 	** NOT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY. THE AUTHOR
677 	** OF THIS SOFTWARE, ASSUMES _NO_ RESPONSIBILITY FOR ANY
678 	** CONSEQUENCE RESULTING FROM THE USE, MODIFICATION, OR
679 	** REDISTRIBUTION OF THIS SOFTWARE.
680 	**
681 	*/
682 
683