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