using System; using System.IO; using System.Text; using System.Threading; using AMWD.Common.Packing.Tar.Interfaces; namespace AMWD.Common.Packing.Tar.Utils { /// /// Implements a legacy TAR writer. /// /// /// Writes tar (see GNU tar) archive to a stream ///
/// Copied from: DotnetMakeDeb ///
/// stream to write archive to public class LegacyTarWriter(Stream outStream) : IDisposable { private readonly Stream _outStream = outStream; private bool _isClosed; /// /// The buffer for writing. /// protected byte[] buffer = new byte[1024]; /// /// Gets or sets a value indicating whether to read on zero. /// public bool ReadOnZero { get; set; } = true; /// /// Gets the output stream. /// protected virtual Stream OutStream { get { return _outStream; } } #region IDisposable Members /// public void Dispose() => Close(); #endregion IDisposable Members /// /// Writes a directory entry. /// /// The path to the directory. /// The user id. /// The group id. /// The access mode. /// is not set. public void WriteDirectoryEntry(string path, int userId, int groupId, int mode) { if (string.IsNullOrEmpty(path)) throw new ArgumentNullException(nameof(path), "The path is not set."); if (path[path.Length - 1] != '/') path += '/'; DateTime lastWriteTime; if (Directory.Exists(path)) { lastWriteTime = Directory.GetLastWriteTime(path); } else { lastWriteTime = DateTime.Now; } // handle long path names (> 99 characters) if (path.Length > 99) { WriteLongName( name: path, userId: userId, groupId: groupId, mode: mode, lastModificationTime: lastWriteTime); // shorten the path name so it can be written properly path = path.Substring(0, 99); } WriteHeader(path, lastWriteTime, 0, userId, groupId, mode, EntryType.Directory); } /// /// Writes a directory and its contents. /// /// The directory. /// Write also sub-directories. /// is not set. public void WriteDirectory(string directory, bool doRecursive) { if (string.IsNullOrEmpty(directory)) throw new ArgumentNullException(nameof(directory), "The directory is not set."); WriteDirectoryEntry(directory, 0, 0, 0755); string[] files = Directory.GetFiles(directory); foreach (string fileName in files) Write(fileName); string[] directories = Directory.GetDirectories(directory); foreach (string dirName in directories) { WriteDirectoryEntry(dirName, 0, 0, 0755); if (doRecursive) WriteDirectory(dirName, true); } } /// /// Writes a file. /// /// The file. /// is not set. public void Write(string fileName) { if (string.IsNullOrEmpty(fileName)) throw new ArgumentNullException(nameof(fileName), "The file name is not set."); using var fileStream = File.OpenRead(fileName); Write(fileStream, fileStream.Length, fileName, 61, 61, 511, File.GetLastWriteTime(fileStream.Name)); } /// /// Writes a file stream. /// /// The file stream. public void Write(FileStream file) { string path = Path.GetFullPath(file.Name).Replace(Path.GetPathRoot(file.Name), string.Empty); path = path.Replace(Path.DirectorySeparatorChar, '/'); Write(file, file.Length, path, 61, 61, 511, File.GetLastWriteTime(file.Name)); } /// /// Writes a stream. /// /// The contents. /// The file size in bytes. /// The file name. public void Write(Stream data, long dataSizeInBytes, string name) => Write(data, dataSizeInBytes, name, 61, 61, 511, DateTime.Now); /// /// Writes a file to the archive. /// /// The file name. /// The file size in bytes. /// The user id. /// The group id. /// The access mode. /// The last modification timestamp. /// The . public virtual void Write(string name, long dataSizeInBytes, int userId, int groupId, int mode, DateTime lastModificationTime, WriteDataDelegate writeDelegate) { var writer = new DataWriter(OutStream, dataSizeInBytes); WriteHeader(name, lastModificationTime, dataSizeInBytes, userId, groupId, mode, EntryType.File); while (writer.CanWrite) writeDelegate(writer); AlignTo512(dataSizeInBytes, false); } /// /// Writes a stream as file to the archive. /// /// The content as . /// The file size in bytes. /// The file name. /// The user id. /// The group id. /// The access mode. /// The last modification timestamp. /// This writer is already closed. public virtual void Write(Stream data, long dataSizeInBytes, string name, int userId, int groupId, int mode, DateTime lastModificationTime) { if (_isClosed) throw new TarException("Can not write to the closed writer"); // handle long file names (> 99 characters) if (name.Length > 99) { WriteLongName( name: name, userId: userId, groupId: groupId, mode: mode, lastModificationTime: lastModificationTime); // shorten the file name so it can be written properly name = name.Substring(0, 99); } WriteHeader(name, lastModificationTime, dataSizeInBytes, userId, groupId, mode, EntryType.File); WriteContent(dataSizeInBytes, data); AlignTo512(dataSizeInBytes, false); } /// /// Handle long file or path names (> 99 characters). /// Write header and content, which its content contain the long (complete) file/path name. /// This handling method is adapted from https://github.com/qmfrederik/dotnet-packaging/pull/50/files#diff-f64c58cc18e8e445cee6ffed7a0d765cdb442c0ef21a3ed80bd20514057967b1 /// /// File name or path name. /// User ID. /// Group ID. /// Mode. /// Last modification time. private void WriteLongName(string name, int userId, int groupId, int mode, DateTime lastModificationTime) { // must include a trailing \0 int nameLength = Encoding.UTF8.GetByteCount(name); byte[] entryName = new byte[nameLength + 1]; Encoding.UTF8.GetBytes(name, 0, name.Length, entryName, 0); // add a "././@LongLink" pseudo-entry which contains the full name using var nameStream = new MemoryStream(entryName); WriteHeader("././@LongLink", lastModificationTime, entryName.Length, userId, groupId, mode, EntryType.LongName); WriteContent(entryName.Length, nameStream); AlignTo512(entryName.Length, false); } /// /// Writes a stream as file to the archive. /// /// The size of the file in bytes. /// The file content as stream. /// has not enough to read from. protected void WriteContent(long count, Stream data) { while (count > 0 && count > buffer.Length) { int bytesRead = data.Read(buffer, 0, buffer.Length); if (bytesRead < 0) throw new IOException($"{nameof(LegacyTarWriter)} unable to read from provided stream"); if (bytesRead == 0) { if (ReadOnZero) Thread.Sleep(100); else break; } OutStream.Write(buffer, 0, bytesRead); count -= bytesRead; } if (count > 0) { int bytesRead = data.Read(buffer, 0, (int)count); if (bytesRead < 0) throw new IOException($"{nameof(LegacyTarWriter)} unable to read from provided stream"); if (bytesRead == 0) { while (count > 0) { OutStream.WriteByte(0); --count; } } else OutStream.Write(buffer, 0, bytesRead); } } /// /// Writes a entry header to the archive. /// /// The file name. /// The last modification time. /// The number of bytes. /// The user id. /// The group id. /// The file mode. /// The entry type protected virtual void WriteHeader(string name, DateTime lastModificationTime, long count, int userId, int groupId, int mode, EntryType entryType) { var header = new TarHeader { FileName = name, LastModification = lastModificationTime, SizeInBytes = count, UserId = userId, GroupId = groupId, Mode = mode, EntryType = entryType }; OutStream.Write(header.GetHeaderValue(), 0, header.HeaderSize); } /// /// Aligns the entry to 512 bytes. /// public void AlignTo512(long size, bool acceptZero) { size %= 512; if (size == 0 && !acceptZero) return; while (size < 512) { OutStream.WriteByte(0); size++; } } /// /// Closes the writer and aligns to 512 bytes. /// public virtual void Close() { if (_isClosed) return; AlignTo512(0, true); AlignTo512(0, true); _isClosed = true; } } }