@@ -54,7 +54,7 @@ public void Dispose()
54
54
#endregion
55
55
56
56
public void Write (
57
- string fileName ,
57
+ FilePath filePath ,
58
58
Stream data ,
59
59
DateTimeOffset modificationTime ,
60
60
int mode ,
@@ -64,18 +64,28 @@ public void Write(
64
64
string userName ,
65
65
string groupName ,
66
66
string deviceMajorNumber ,
67
- string deviceMinorNumber )
67
+ string deviceMinorNumber ,
68
+ string entrySha ,
69
+ bool isLink )
68
70
{
69
- WriteHeader ( fileName , modificationTime , ( data != null ) ? data . Length : 0 , mode , userId , groupId , typeflag , userName , groupName , deviceMajorNumber , deviceMinorNumber ) ;
70
- // folders have no data
71
- if ( data != null )
71
+ FileNameExtendedHeader fileNameExtendedHeader = FileNameExtendedHeader . Parse ( filePath . Posix , entrySha ) ;
72
+ LinkExtendedHeader linkExtendedHeader = ParseLink ( isLink , data , entrySha ) ;
73
+
74
+ WriteExtendedHeader ( fileNameExtendedHeader , linkExtendedHeader , entrySha , modificationTime ) ;
75
+
76
+ // Note: in case of links, we won't add a content, but the size in the header will still be != 0. It seems strange, but it seem to be what git.git is doing?
77
+ WriteHeader ( fileNameExtendedHeader . Name , fileNameExtendedHeader . Prefix , modificationTime , ( data != null ) ? data . Length : 0 , mode ,
78
+ userId , groupId , typeflag , linkExtendedHeader . Link , userName , groupName , deviceMajorNumber , deviceMinorNumber ) ;
79
+
80
+ // folders have no data, and so do links
81
+ if ( data != null && ! isLink )
72
82
{
73
- WriteContent ( data . Length , data ) ;
83
+ WriteContent ( data . Length , data , OutStream ) ;
74
84
}
75
- AlignTo512 ( data . Length , false ) ;
85
+ AlignTo512 ( ( data != null ) ? data . Length : 0 , false ) ;
76
86
}
77
87
78
- protected void WriteContent ( long count , Stream data )
88
+ protected void WriteContent ( long count , Stream data , Stream dest )
79
89
{
80
90
var buffer = new byte [ 1024 ] ;
81
91
@@ -85,7 +95,7 @@ protected void WriteContent(long count, Stream data)
85
95
if ( bytesRead < 0 )
86
96
throw new IOException ( "TarWriter unable to read from provided stream" ) ;
87
97
88
- OutStream . Write ( buffer , 0 , bytesRead ) ;
98
+ dest . Write ( buffer , 0 , bytesRead ) ;
89
99
count -= bytesRead ;
90
100
}
91
101
if ( count > 0 )
@@ -97,12 +107,12 @@ protected void WriteContent(long count, Stream data)
97
107
{
98
108
while ( count > 0 )
99
109
{
100
- OutStream . WriteByte ( 0 ) ;
110
+ dest . WriteByte ( 0 ) ;
101
111
-- count ;
102
112
}
103
113
}
104
114
else
105
- OutStream . Write ( buffer , 0 , bytesRead ) ;
115
+ dest . Write ( buffer , 0 , bytesRead ) ;
106
116
}
107
117
}
108
118
@@ -118,23 +128,89 @@ protected void AlignTo512(long size, bool acceptZero)
118
128
}
119
129
120
130
protected void WriteHeader (
121
- string name ,
131
+ string fileName ,
132
+ string namePrefix ,
122
133
DateTimeOffset lastModificationTime ,
123
134
long count ,
124
135
int mode ,
125
136
string userId ,
126
137
string groupId ,
127
138
char typeflag ,
139
+ string link ,
128
140
string userName ,
129
141
string groupName ,
130
142
string deviceMajorNumber ,
131
143
string deviceMinorNumber )
132
144
{
133
- var tarHeader = new UsTarHeader ( name , lastModificationTime , count , mode , userId , groupId , typeflag , userName , groupName , deviceMajorNumber , deviceMinorNumber ) ;
145
+ var tarHeader = new UsTarHeader ( fileName , namePrefix , lastModificationTime , count , mode ,
146
+ userId , groupId , typeflag , link , userName , groupName , deviceMajorNumber , deviceMinorNumber ) ;
134
147
var header = tarHeader . GetHeaderValue ( ) ;
135
148
OutStream . Write ( header , 0 , header . Length ) ;
136
149
}
137
150
151
+ private LinkExtendedHeader ParseLink ( bool isLink , Stream data , string entrySha )
152
+ {
153
+ if ( ! isLink )
154
+ {
155
+ return new LinkExtendedHeader ( string . Empty , string . Empty , false ) ;
156
+ }
157
+
158
+ using ( var dest = new MemoryStream ( ) )
159
+ {
160
+ WriteContent ( data . Length , data , dest ) ;
161
+ dest . Seek ( 0 , SeekOrigin . Begin ) ;
162
+
163
+ using ( var linkStream = new StreamReader ( dest ) )
164
+ {
165
+ string link = linkStream . ReadToEnd ( ) ;
166
+
167
+ if ( data . Length > 100 )
168
+ {
169
+ return new LinkExtendedHeader ( link , string . Format ( "see %s.paxheader{0}" , entrySha ) , true ) ;
170
+ }
171
+
172
+ return new LinkExtendedHeader ( link , link , false ) ;
173
+ }
174
+ }
175
+ }
176
+
177
+ private void WriteExtendedHeader ( FileNameExtendedHeader fileNameExtendedHeader , LinkExtendedHeader linkExtendedHeader , string entrySha ,
178
+ DateTimeOffset modificationTime )
179
+ {
180
+ string extHeader = string . Empty ;
181
+
182
+ if ( fileNameExtendedHeader . NeedsExtendedHeaderEntry )
183
+ {
184
+ extHeader += BuildKeyValueExtHeader ( "path" , fileNameExtendedHeader . InitialPath ) ;
185
+ }
186
+
187
+ if ( linkExtendedHeader . NeedsExtendedHeaderEntry )
188
+ {
189
+ extHeader += BuildKeyValueExtHeader ( "linkpath" , linkExtendedHeader . InitialLink ) ;
190
+ }
191
+
192
+ if ( string . IsNullOrEmpty ( extHeader ) )
193
+ {
194
+ return ;
195
+ }
196
+
197
+ using ( var stream = new MemoryStream ( Encoding . ASCII . GetBytes ( extHeader ) ) )
198
+ {
199
+ Write ( string . Format ( "{0}.paxheader" , entrySha ) , stream , modificationTime , "666" . OctalToInt32 ( ) ,
200
+ "0" , "0" , 'x' , "root" , "root" , "0" , "0" , entrySha , false ) ;
201
+ }
202
+ }
203
+
204
+ private string BuildKeyValueExtHeader ( string key , string value )
205
+ {
206
+ // "%u %s=%s\n"
207
+ int len = key . Length + value . Length + 3 ;
208
+ for ( int i = len ; i > 9 ; i /= 10 )
209
+ len ++ ;
210
+
211
+ return string . Format ( "{0} {1}={2}\n " , len , key , value ) ;
212
+ }
213
+
138
214
/// <summary>
139
215
/// UsTar header implementation.
140
216
/// </summary>
@@ -150,20 +226,22 @@ private class UsTarHeader
150
226
private readonly string userId ;
151
227
private readonly string groupId ;
152
228
private readonly char typeflag ;
229
+ private readonly string link ;
153
230
private readonly string deviceMajorNumber ;
154
231
private readonly string deviceMinorNumber ;
155
- private string namePrefix ;
156
- private string fileName ;
157
-
232
+ private readonly string namePrefix ;
233
+ private readonly string fileName ;
158
234
159
235
public UsTarHeader (
160
- string filePath ,
236
+ string fileName ,
237
+ string namePrefix ,
161
238
DateTimeOffset lastModificationTime ,
162
239
long size ,
163
240
int mode ,
164
241
string userId ,
165
242
string groupId ,
166
243
char typeflag ,
244
+ string link ,
167
245
string userName ,
168
246
string groupName ,
169
247
string deviceMajorNumber ,
@@ -195,6 +273,10 @@ public UsTarHeader(
195
273
{
196
274
throw new ArgumentException ( "ustar deviceMinorNumber cannot be longer than 7 characters." , "deviceMinorNumber" ) ;
197
275
}
276
+ if ( link . Length > 100 )
277
+ {
278
+ throw new ArgumentException ( "ustar link cannot be longer than 100 characters." , "link" ) ;
279
+ }
198
280
199
281
#endregion
200
282
@@ -206,43 +288,12 @@ public UsTarHeader(
206
288
this . userName = userName ;
207
289
this . groupName = groupName ;
208
290
this . typeflag = typeflag ;
291
+ this . link = link ;
209
292
this . deviceMajorNumber = deviceMajorNumber . PadLeft ( 7 , '0' ) ;
210
293
this . deviceMinorNumber = deviceMinorNumber . PadLeft ( 7 , '0' ) ;
211
-
212
- ParseFileName ( filePath ) ;
213
- }
214
-
215
- private void ParseFileName ( string fullFilePath )
216
- {
217
- var posixPath = fullFilePath . Replace ( '/' , Path . DirectorySeparatorChar ) ;
218
-
219
- if ( posixPath . Length > 100 )
220
- {
221
- if ( posixPath . Length > 255 )
222
- {
223
- throw new ArgumentException ( string . Format ( "ustar filePath ({0}) cannot be longer than 255 chars" , posixPath ) ) ;
224
- }
225
- int position = posixPath . Length - 100 ;
226
294
227
- // Find first path separator in the remaining 100 chars of the file name
228
- while ( ! Equals ( '/' , posixPath [ position ] ) )
229
- {
230
- ++ position ;
231
- if ( position == posixPath . Length )
232
- {
233
- break ;
234
- }
235
- }
236
- if ( position == posixPath . Length )
237
- position = posixPath . Length - 100 ;
238
- namePrefix = posixPath . Substring ( 0 , position ) ;
239
- fileName = posixPath . Substring ( position , posixPath . Length - position ) ;
240
- }
241
- else
242
- {
243
- namePrefix = string . Empty ;
244
- fileName = posixPath ;
245
- }
295
+ this . fileName = fileName ;
296
+ this . namePrefix = namePrefix ;
246
297
}
247
298
248
299
public byte [ ] GetHeaderValue ( )
@@ -257,6 +308,7 @@ public byte[] GetHeaderValue()
257
308
Encoding . ASCII . GetBytes ( Convert . ToString ( size , 8 ) . PadLeft ( 11 , '0' ) ) . CopyTo ( buffer , 124 ) ;
258
309
Encoding . ASCII . GetBytes ( unixTime ) . CopyTo ( buffer , 136 ) ;
259
310
buffer [ 156 ] = Convert . ToByte ( typeflag ) ;
311
+ Encoding . ASCII . GetBytes ( link ) . CopyTo ( buffer , 157 ) ;
260
312
261
313
Encoding . ASCII . GetBytes ( magic ) . CopyTo ( buffer , 257 ) ; // Mark header as ustar
262
314
Encoding . ASCII . GetBytes ( version ) . CopyTo ( buffer , 263 ) ;
@@ -301,5 +353,95 @@ private static byte[] AlignTo12(byte[] bytes)
301
353
return retVal ;
302
354
}
303
355
}
356
+
357
+ private class FileNameExtendedHeader
358
+ {
359
+ private readonly string prefix ;
360
+ private readonly string name ;
361
+ private readonly string initialPath ;
362
+ private readonly bool needsExtendedHeaderEntry ;
363
+
364
+ private FileNameExtendedHeader ( string initialPath , string prefix , string name , bool needsExtendedHeaderEntry )
365
+ {
366
+ this . initialPath = initialPath ;
367
+ this . prefix = prefix ;
368
+ this . name = name ;
369
+ this . needsExtendedHeaderEntry = needsExtendedHeaderEntry ;
370
+ }
371
+
372
+ public bool NeedsExtendedHeaderEntry
373
+ {
374
+ get { return needsExtendedHeaderEntry ; }
375
+ }
376
+
377
+ public string Name
378
+ {
379
+ get { return name ; }
380
+ }
381
+
382
+ public string Prefix
383
+ {
384
+ get { return prefix ; }
385
+ }
386
+
387
+ public string InitialPath
388
+ {
389
+ get { return initialPath ; }
390
+ }
391
+
392
+ /// <summary>
393
+ /// Logic taken from https://github.com/git/git/blob/master/archive-tar.c
394
+ /// </summary>
395
+ public static FileNameExtendedHeader Parse ( string posixPath , string entrySha )
396
+ {
397
+ if ( posixPath . Length > 100 )
398
+ {
399
+ // Need to increment by one because while loop decrements first before testing for path separator
400
+ int position = Math . Min ( 156 , posixPath . Length ) ;
401
+
402
+ while ( -- position > 0 && ! Equals ( '/' , posixPath [ position ] ) )
403
+ { }
404
+
405
+ int remaining = posixPath . Length - position - 1 ;
406
+ if ( remaining < 100 && position > 0 )
407
+ {
408
+ return new FileNameExtendedHeader ( posixPath , posixPath . Substring ( 0 , position ) , posixPath . Substring ( position , posixPath . Length - position ) , false ) ;
409
+ }
410
+
411
+ return new FileNameExtendedHeader ( posixPath , string . Empty , string . Format ( "{0}.data" , entrySha ) , true ) ;
412
+ }
413
+
414
+ return new FileNameExtendedHeader ( posixPath , string . Empty , posixPath , false ) ;
415
+ }
416
+ }
417
+
418
+ private class LinkExtendedHeader
419
+ {
420
+ private readonly string initialLink ;
421
+ private readonly string link ;
422
+ private readonly bool needsExtendedHeaderEntry ;
423
+
424
+ public LinkExtendedHeader ( string initialLink , string link , bool needsExtendedHeaderEntry )
425
+ {
426
+ this . initialLink = initialLink ;
427
+ this . link = link ;
428
+ this . needsExtendedHeaderEntry = needsExtendedHeaderEntry ;
429
+ }
430
+
431
+ public string InitialLink
432
+ {
433
+ get { return initialLink ; }
434
+ }
435
+
436
+ public bool NeedsExtendedHeaderEntry
437
+ {
438
+ get { return needsExtendedHeaderEntry ; }
439
+ }
440
+
441
+ public string Link
442
+ {
443
+ get { return link ; }
444
+ }
445
+ }
304
446
}
305
447
}
0 commit comments