1- // source https://gist.github.com/Su-s/438be493ae692318c73e30367cbc5c2a
2- // updated source https://gist.github.com/Matheos96/da8990030dfe3e27b0a48722042d9c0b
3-
41using System ;
52using System . IO ;
63using System . IO . Compression ;
@@ -11,10 +8,8 @@ namespace TarLib
118 public class Tar
129 {
1310 /// <summary>
14- /// Extracts a <i> .tar.gz</i> archive to the specified directory.
11+ /// Extracts a .tar.gz archive to the specified directory.
1512 /// </summary>
16- /// <param name="filename">The <i>.tar.gz</i> to decompress and extract.</param>
17- /// <param name="outputDir">Output directory to write the files.</param>
1813 public static void ExtractTarGz ( string filename , string outputDir )
1914 {
2015 using ( var stream = File . OpenRead ( filename ) )
@@ -24,39 +19,29 @@ public static void ExtractTarGz(string filename, string outputDir)
2419 }
2520
2621 /// <summary>
27- /// Extracts a <i> .tar.gz</i> archive stream to the specified directory.
22+ /// Extracts a .tar.gz archive stream to the specified directory.
2823 /// </summary>
29- /// <param name="stream">The <i>.tar.gz</i> to decompress and extract.</param>
30- /// <param name="outputDir">Output directory to write the files.</param>
3124 public static void ExtractTarGz ( Stream stream , string outputDir )
3225 {
33- int read ;
34- const int chunk = 4096 ;
26+ const int chunk = 4096 * 4 ;
3527 var buffer = new byte [ chunk ] ;
3628
37- // A GZipStream is not seekable, so copy it first to a MemoryStream
3829 using ( var gzipStream = new GZipStream ( stream , CompressionMode . Decompress ) )
30+ using ( var memStream = new MemoryStream ( ) )
3931 {
40- using ( var memStream = new MemoryStream ( ) )
32+ int read ;
33+ while ( ( read = gzipStream . Read ( buffer , 0 , buffer . Length ) ) > 0 )
4134 {
42- //For .NET 6+
43- while ( ( read = gzipStream . Read ( buffer , 0 , buffer . Length ) ) > 0 )
44- {
45- memStream . Write ( buffer , 0 , read ) ;
46- }
47- memStream . Seek ( 0 , SeekOrigin . Begin ) ;
48-
49- //ExtractTar(gzip, outputDir);
50- ExtractTar ( memStream , outputDir ) ;
35+ memStream . Write ( buffer , 0 , read ) ;
5136 }
37+ memStream . Seek ( 0 , SeekOrigin . Begin ) ;
38+ ExtractTar ( memStream , outputDir ) ;
5239 }
5340 }
5441
5542 /// <summary>
56- /// Extractes a <c> tar</c> archive to the specified directory .
43+ /// Extracts a tar archive file .
5744 /// </summary>
58- /// <param name="filename">The <i>.tar</i> to extract.</param>
59- /// <param name="outputDir">Output directory to write the files.</param>
6045 public static void ExtractTar ( string filename , string outputDir )
6146 {
6247 using ( var stream = File . OpenRead ( filename ) )
@@ -66,85 +51,206 @@ public static void ExtractTar(string filename, string outputDir)
6651 }
6752
6853 /// <summary>
69- /// Extractes a <c>tar</c> archive to the specified directory.
54+ /// Extracts a tar archive stream.
55+ /// Fixes path loss caused by ignoring the POSIX 'prefix' field and wrong header offsets.
7056 /// </summary>
71- /// <param name="stream">The <i>.tar</i> to extract.</param>
72- /// <param name="outputDir">Output directory to write the files.</param>
7357 public static void ExtractTar ( Stream stream , string outputDir )
7458 {
75- var buffer = new byte [ 100 ] ;
76- var longFileName = string . Empty ;
59+ // Tar header constants
60+ const int HeaderSize = 512 ;
61+ byte [ ] header = new byte [ HeaderSize ] ;
62+
63+ string pendingLongName = null ; // For GNU long name ('L') entries
64+
7765 while ( true )
7866 {
79- stream . Read ( buffer , 0 , 100 ) ;
80- string name = string . IsNullOrEmpty ( longFileName ) ? Encoding . ASCII . GetString ( buffer ) . Trim ( '\0 ' ) : longFileName ; //Use longFileName if we have one read
81-
82- if ( String . IsNullOrWhiteSpace ( name ) ) break ;
83- stream . Seek ( 24 , SeekOrigin . Current ) ;
84- stream . Read ( buffer , 0 , 12 ) ;
85- var size = Convert . ToInt64 ( Encoding . UTF8 . GetString ( buffer , 0 , 12 ) . Trim ( '\0 ' ) . Trim ( ) , 8 ) ;
86- stream . Seek ( 20 , SeekOrigin . Current ) ; //Move head to typeTag byte
87- var typeTag = stream . ReadByte ( ) ;
88- stream . Seek ( 355L , SeekOrigin . Current ) ; //Move head to beginning of data (byte 512)
89-
90- if ( typeTag == 'L' )
67+ int bytesRead = ReadExact ( stream , header , 0 , HeaderSize ) ;
68+ if ( bytesRead == 0 ) break ; // End of stream
69+ if ( bytesRead < HeaderSize ) throw new EndOfStreamException ( "Unexpected end of tar stream." ) ;
70+
71+ // Detect two consecutive zero blocks (end of archive)
72+ bool allZero = IsAllZero ( header ) ;
73+ if ( allZero )
74+ {
75+ // Peek next block; if also zero -> end
76+ bytesRead = ReadExact ( stream , header , 0 , HeaderSize ) ;
77+ if ( bytesRead == 0 || IsAllZero ( header ) ) break ;
78+ if ( bytesRead < HeaderSize ) throw new EndOfStreamException ( "Unexpected end of tar stream." ) ;
79+ }
80+
81+ // Parse fields (POSIX ustar)
82+ string name = GetString ( header , 0 , 100 ) ;
83+ string mode = GetString ( header , 100 , 8 ) ;
84+ string uid = GetString ( header , 108 , 8 ) ;
85+ string gid = GetString ( header , 116 , 8 ) ;
86+ string sizeOctal = GetString ( header , 124 , 12 ) ;
87+ string mtime = GetString ( header , 136 , 12 ) ;
88+ string checksum = GetString ( header , 148 , 8 ) ;
89+ char typeFlag = ( char ) header [ 156 ] ;
90+ string linkName = GetString ( header , 157 , 100 ) ;
91+ string magic = GetString ( header , 257 , 6 ) ; // "ustar\0" or "ustar "
92+ string version = GetString ( header , 263 , 2 ) ;
93+ string uname = GetString ( header , 265 , 32 ) ;
94+ string gname = GetString ( header , 297 , 32 ) ;
95+ string prefix = GetString ( header , 345 , 155 ) ;
96+
97+ // Compose full name using prefix (if present and not using GNU long name override)
98+ if ( ! string . IsNullOrEmpty ( prefix ) )
99+ {
100+ name = prefix + "/" + name ;
101+ }
102+
103+ // If we previously read a GNU long name block, override current name
104+ if ( ! string . IsNullOrEmpty ( pendingLongName ) )
91105 {
92- //If Type Tag is 'L' we have a filename that is longer than the 100 bytes reserved for it in the header.
93- //We read it here and save it temporarily as it will be the file name of the next block where the actual data is
94- var buf = new byte [ size ] ;
95- stream . Read ( buf , 0 , buf . Length ) ;
96- longFileName = Encoding . ASCII . GetString ( buf ) . Trim ( '\0 ' ) ;
106+ name = pendingLongName ;
107+ pendingLongName = null ;
97108 }
98- else
109+
110+ long size = ParseOctal ( sizeOctal ) ;
111+
112+ // Handle GNU long name extension block: the data of this entry is the filename of next entry.
113+ if ( typeFlag == 'L' )
99114 {
100- longFileName = string . Empty ; //Reset longFileName if current entry is not indicating one
115+ byte [ ] longNameData = new byte [ size ] ;
116+ ReadExact ( stream , longNameData , 0 , ( int ) size ) ;
117+ pendingLongName = Encoding . ASCII . GetString ( longNameData ) . Trim ( '\0 ' , '\r ' , '\n ' ) ;
118+ SkipPadding ( stream , size ) ;
119+ continue ; // Move to next header
120+ }
121+
122+ // Skip PAX extended header (type 'x') - metadata only
123+ if ( typeFlag == 'x' )
124+ {
125+ SkipData ( stream , size ) ;
126+ SkipPadding ( stream , size ) ;
127+ continue ;
128+ }
129+
130+ // Normalize name
131+ if ( string . IsNullOrWhiteSpace ( name ) ) continue ;
101132
102- var output = Path . Combine ( outputDir , name ) ;
133+ // Directory?
134+ bool isDirectory = typeFlag == '5' || name . EndsWith ( "/" ) ;
103135
104- // only include these folders
105- var include = ( output . IndexOf ( "package/ProjectData~/Assets/" ) > - 1 ) ;
106- include |= ( output . IndexOf ( "package/ProjectData~/ProjectSettings/" ) > - 1 ) ;
107- include |= ( output . IndexOf ( "package/ProjectData~/Packages/" ) > - 1 ) ;
136+ // Inclusion filter (original logic)
137+ string originalName = name ;
138+ bool include =
139+ originalName . IndexOf ( "package/ProjectData~/Assets/" , StringComparison . Ordinal ) > - 1 ||
140+ originalName . IndexOf ( "package/ProjectData~/ProjectSettings/" , StringComparison . Ordinal ) > - 1 ||
141+ originalName . IndexOf ( "package/ProjectData~/Library/" , StringComparison . Ordinal ) > - 1 ||
142+ originalName . IndexOf ( "package/ProjectData~/Packages/" , StringComparison . Ordinal ) > - 1 ;
108143
109- // rename output path from "package/ProjectData~/Assets/" into "Assets/"
110- output = output . Replace ( "package/ProjectData~/" , "" ) ;
144+ // Strip leading prefix.
145+ string cleanedName = originalName . StartsWith ( "package/ProjectData~/" , StringComparison . Ordinal )
146+ ? originalName . Substring ( "package/ProjectData~/" . Length )
147+ : originalName ;
148+
149+ string finalPath = Path . Combine ( outputDir , cleanedName . Replace ( '/' , Path . DirectorySeparatorChar ) ) ;
150+
151+ if ( isDirectory )
152+ {
153+ if ( include && ! Directory . Exists ( finalPath ) )
154+ Directory . CreateDirectory ( finalPath ) ;
155+ // No data to read for directory; continue to next header
156+ SkipData ( stream , size ) ; // size should be 0
157+ SkipPadding ( stream , size ) ;
158+ continue ;
159+ }
160+
161+ // Ensure directory exists
162+ if ( include )
163+ {
164+ string dir = Path . GetDirectoryName ( finalPath ) ;
165+ if ( ! string . IsNullOrEmpty ( dir ) && ! Directory . Exists ( dir ) )
166+ Directory . CreateDirectory ( dir ) ;
167+ }
111168
112- if ( include == true && ! Directory . Exists ( Path . GetDirectoryName ( output ) ) ) Directory . CreateDirectory ( Path . GetDirectoryName ( output ) ) ;
169+ // Read file data (always advance stream even if not included)
170+ byte [ ] fileData = new byte [ size ] ;
171+ ReadExact ( stream , fileData , 0 , ( int ) size ) ;
113172
114- // not folder
115- //if (name.Equals("./", StringComparison.InvariantCulture) == false)
116- if ( name . EndsWith ( "/" ) == false ) //Directories are zero size and don't need anything written
173+ if ( include )
174+ {
175+ using ( var fs = File . Open ( finalPath , FileMode . Create , FileAccess . Write ) )
117176 {
118- if ( include == true )
119- {
120- //Console.WriteLine("output=" + output);
121- using ( var str = File . Open ( output , FileMode . OpenOrCreate , FileAccess . ReadWrite ) )
122- {
123- var buf = new byte [ size ] ;
124- stream . Read ( buf , 0 , buf . Length ) ;
125- // take only data from this folder
126- str . Write ( buf , 0 , buf . Length ) ;
127- }
128- }
129- else
130- {
131- var buf = new byte [ size ] ;
132- stream . Read ( buf , 0 , buf . Length ) ;
133- }
177+ fs . Write ( fileData , 0 , fileData . Length ) ;
134178 }
135179 }
136180
137- //Move head to next 512 byte block
138- var pos = stream . Position ;
139- var offset = 512 - ( pos % 512 ) ;
140- if ( offset == 512 ) offset = 0 ;
181+ // Skip padding to 512 boundary
182+ SkipPadding ( stream , size ) ;
183+ }
184+ }
141185
142- stream . Seek ( offset , SeekOrigin . Current ) ;
186+ private static string GetString ( byte [ ] buffer , int offset , int length )
187+ {
188+ var s = Encoding . ASCII . GetString ( buffer , offset , length ) ;
189+ int nullIndex = s . IndexOf ( '\0 ' ) ;
190+ if ( nullIndex >= 0 ) s = s . Substring ( 0 , nullIndex ) ;
191+ return s . Trim ( ) ;
192+ }
193+
194+ private static long ParseOctal ( string s )
195+ {
196+ s = s . Trim ( ) ;
197+ if ( string . IsNullOrEmpty ( s ) ) return 0 ;
198+ try
199+ {
200+ return Convert . ToInt64 ( s , 8 ) ;
201+ }
202+ catch
203+ {
204+ // Fallback: treat as decimal if malformed
205+ long val ;
206+ return long . TryParse ( s , out val ) ? val : 0 ;
143207 }
144208 }
145- } // class Tar
146- } // namespace TarLib
147209
210+ private static bool IsAllZero ( byte [ ] buffer )
211+ {
212+ for ( int i = 0 ; i < buffer . Length ; i ++ )
213+ if ( buffer [ i ] != 0 ) return false ;
214+ return true ;
215+ }
216+
217+ private static int ReadExact ( Stream stream , byte [ ] buffer , int offset , int count )
218+ {
219+ int total = 0 ;
220+ while ( total < count )
221+ {
222+ int read = stream . Read ( buffer , offset + total , count - total ) ;
223+ if ( read <= 0 ) break ;
224+ total += read ;
225+ }
226+ return total ;
227+ }
228+
229+ private static void SkipData ( Stream stream , long size )
230+ {
231+ if ( size <= 0 ) return ;
232+ const int chunk = 8192 ;
233+ byte [ ] tmp = new byte [ Math . Min ( chunk , ( int ) size ) ] ;
234+ long remaining = size ;
235+ while ( remaining > 0 )
236+ {
237+ int toRead = ( int ) Math . Min ( tmp . Length , remaining ) ;
238+ int read = stream . Read ( tmp , 0 , toRead ) ;
239+ if ( read <= 0 ) throw new EndOfStreamException ( "Unexpected end while skipping data." ) ;
240+ remaining -= read ;
241+ }
242+ }
243+
244+ private static void SkipPadding ( Stream stream , long size )
245+ {
246+ long padding = ( 512 - ( size % 512 ) ) % 512 ;
247+ if ( padding > 0 )
248+ {
249+ stream . Seek ( padding , SeekOrigin . Current ) ;
250+ }
251+ }
252+ }
253+ }
148254
149255/*
150256This software is available under 2 licenses-- choose whichever you prefer.
@@ -184,4 +290,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
184290AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
185291ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
186292WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
187- */
293+ */
0 commit comments