1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.commons.compress.archivers.tar;
20
21 import static java.nio.charset.StandardCharsets.UTF_8;
22
23 import java.io.File;
24 import java.io.IOException;
25 import java.io.OutputStream;
26 import java.io.StringWriter;
27 import java.math.BigDecimal;
28 import java.math.RoundingMode;
29 import java.nio.ByteBuffer;
30 import java.nio.charset.StandardCharsets;
31 import java.nio.file.LinkOption;
32 import java.nio.file.Path;
33 import java.nio.file.attribute.FileTime;
34 import java.time.Instant;
35 import java.util.HashMap;
36 import java.util.Map;
37
38 import org.apache.commons.compress.archivers.ArchiveOutputStream;
39 import org.apache.commons.compress.archivers.zip.ZipEncoding;
40 import org.apache.commons.compress.archivers.zip.ZipEncodingHelper;
41 import org.apache.commons.compress.utils.FixedLengthBlockOutputStream;
42 import org.apache.commons.compress.utils.TimeUtils;
43 import org.apache.commons.io.Charsets;
44 import org.apache.commons.io.file.attribute.FileTimes;
45 import org.apache.commons.io.output.CountingOutputStream;
46 import org.apache.commons.lang3.ArrayFill;
47
48
49
50
51
52
53
54
55
56
57
58
59
60 public class TarArchiveOutputStream extends ArchiveOutputStream<TarArchiveEntry> {
61
62
63
64
65 public static final int LONGFILE_ERROR = 0;
66
67
68
69
70 public static final int LONGFILE_TRUNCATE = 1;
71
72
73
74
75 public static final int LONGFILE_GNU = 2;
76
77
78
79
80 public static final int LONGFILE_POSIX = 3;
81
82
83
84
85 public static final int BIGNUMBER_ERROR = 0;
86
87
88
89
90 public static final int BIGNUMBER_STAR = 1;
91
92
93
94
95 public static final int BIGNUMBER_POSIX = 2;
96 private static final int RECORD_SIZE = 512;
97
98 private static final ZipEncoding ASCII = ZipEncodingHelper.getZipEncoding(StandardCharsets.US_ASCII);
99
100 private static final int BLOCK_SIZE_UNSPECIFIED = -511;
101 private long currSize;
102 private String currName;
103 private long currBytes;
104 private final byte[] recordBuf;
105 private int longFileMode = LONGFILE_ERROR;
106 private int bigNumberMode = BIGNUMBER_ERROR;
107
108 private long recordsWritten;
109
110 private final int recordsPerBlock;
111
112
113
114
115 private boolean haveUnclosedEntry;
116
117 private final CountingOutputStream countingOut;
118
119 private final ZipEncoding zipEncoding;
120
121
122
123
124 final String charsetName;
125
126 private boolean addPaxHeadersForNonAsciiNames;
127
128
129
130
131
132
133
134
135
136
137 public TarArchiveOutputStream(final OutputStream os) {
138 this(os, BLOCK_SIZE_UNSPECIFIED);
139 }
140
141
142
143
144
145
146
147 public TarArchiveOutputStream(final OutputStream os, final int blockSize) {
148 this(os, blockSize, null);
149 }
150
151
152
153
154
155
156
157
158
159 @Deprecated
160 public TarArchiveOutputStream(final OutputStream os, final int blockSize, final int recordSize) {
161 this(os, blockSize, recordSize, null);
162 }
163
164
165
166
167
168
169
170
171
172
173
174 @Deprecated
175 public TarArchiveOutputStream(final OutputStream os, final int blockSize, final int recordSize, final String encoding) {
176 this(os, blockSize, encoding);
177 if (recordSize != RECORD_SIZE) {
178 throw new IllegalArgumentException("Tar record size must always be 512 bytes. Attempt to set size of " + recordSize);
179 }
180
181 }
182
183
184
185
186
187
188
189
190
191 public TarArchiveOutputStream(final OutputStream os, final int blockSize, final String encoding) {
192 super(os);
193 final int realBlockSize;
194 if (BLOCK_SIZE_UNSPECIFIED == blockSize) {
195 realBlockSize = RECORD_SIZE;
196 } else {
197 realBlockSize = blockSize;
198 }
199
200 if (realBlockSize <= 0 || realBlockSize % RECORD_SIZE != 0) {
201 throw new IllegalArgumentException("Block size must be a multiple of 512 bytes. Attempt to use set size of " + blockSize);
202 }
203 this.out = new FixedLengthBlockOutputStream(countingOut = new CountingOutputStream(os), RECORD_SIZE);
204 this.charsetName = Charsets.toCharset(encoding).name();
205 this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding);
206
207 this.recordBuf = new byte[RECORD_SIZE];
208 this.recordsPerBlock = realBlockSize / RECORD_SIZE;
209 }
210
211
212
213
214
215
216
217
218
219
220
221
222 public TarArchiveOutputStream(final OutputStream os, final String encoding) {
223 this(os, BLOCK_SIZE_UNSPECIFIED, encoding);
224 }
225
226 private void addFileTimePaxHeader(final Map<String, String> paxHeaders, final String header, final FileTime value) {
227 if (value != null) {
228 final Instant instant = value.toInstant();
229 final long seconds = instant.getEpochSecond();
230 final int nanos = instant.getNano();
231 if (nanos == 0) {
232 paxHeaders.put(header, String.valueOf(seconds));
233 } else {
234 addInstantPaxHeader(paxHeaders, header, seconds, nanos);
235 }
236 }
237 }
238
239 private void addFileTimePaxHeaderForBigNumber(final Map<String, String> paxHeaders, final String header, final FileTime value, final long maxValue) {
240 if (value != null) {
241 final Instant instant = value.toInstant();
242 final long seconds = instant.getEpochSecond();
243 final int nanos = instant.getNano();
244 if (nanos == 0) {
245 addPaxHeaderForBigNumber(paxHeaders, header, seconds, maxValue);
246 } else {
247 addInstantPaxHeader(paxHeaders, header, seconds, nanos);
248 }
249 }
250 }
251
252 private void addInstantPaxHeader(final Map<String, String> paxHeaders, final String header, final long seconds, final int nanos) {
253 final BigDecimal bdSeconds = BigDecimal.valueOf(seconds);
254 final BigDecimal bdNanos = BigDecimal.valueOf(nanos).movePointLeft(9).setScale(7, RoundingMode.DOWN);
255 final BigDecimal timestamp = bdSeconds.add(bdNanos);
256 paxHeaders.put(header, timestamp.toPlainString());
257 }
258
259 private void addPaxHeaderForBigNumber(final Map<String, String> paxHeaders, final String header, final long value, final long maxValue) {
260 if (value < 0 || value > maxValue) {
261 paxHeaders.put(header, String.valueOf(value));
262 }
263 }
264
265 private void addPaxHeadersForBigNumbers(final Map<String, String> paxHeaders, final TarArchiveEntry entry) {
266 addPaxHeaderForBigNumber(paxHeaders, "size", entry.getSize(), TarConstants.MAXSIZE);
267 addPaxHeaderForBigNumber(paxHeaders, "gid", entry.getLongGroupId(), TarConstants.MAXID);
268 addFileTimePaxHeaderForBigNumber(paxHeaders, "mtime", entry.getLastModifiedTime(), TarConstants.MAXSIZE);
269 addFileTimePaxHeader(paxHeaders, "atime", entry.getLastAccessTime());
270 if (entry.getStatusChangeTime() != null) {
271 addFileTimePaxHeader(paxHeaders, "ctime", entry.getStatusChangeTime());
272 } else {
273
274 addFileTimePaxHeader(paxHeaders, "ctime", entry.getCreationTime());
275 }
276 addPaxHeaderForBigNumber(paxHeaders, "uid", entry.getLongUserId(), TarConstants.MAXID);
277
278 addFileTimePaxHeader(paxHeaders, "LIBARCHIVE.creationtime", entry.getCreationTime());
279
280 addPaxHeaderForBigNumber(paxHeaders, "SCHILY.devmajor", entry.getDevMajor(), TarConstants.MAXID);
281 addPaxHeaderForBigNumber(paxHeaders, "SCHILY.devminor", entry.getDevMinor(), TarConstants.MAXID);
282
283 failForBigNumber("mode", entry.getMode(), TarConstants.MAXID);
284 }
285
286
287
288
289
290
291 @Override
292 public void close() throws IOException {
293 try {
294 if (!isFinished()) {
295 finish();
296 }
297 } finally {
298 if (!isClosed()) {
299 super.close();
300 }
301 }
302 }
303
304
305
306
307
308
309
310
311 @Override
312 public void closeArchiveEntry() throws IOException {
313 checkFinished();
314 if (!haveUnclosedEntry) {
315 throw new IOException("No current entry to close");
316 }
317 ((FixedLengthBlockOutputStream) out).flushBlock();
318 if (currBytes < currSize) {
319 throw new IOException(
320 "Entry '" + currName + "' closed at '" + currBytes + "' before the '" + currSize + "' bytes specified in the header were written");
321 }
322 recordsWritten += currSize / RECORD_SIZE;
323
324 if (0 != currSize % RECORD_SIZE) {
325 recordsWritten++;
326 }
327 haveUnclosedEntry = false;
328 }
329
330 @Override
331 public TarArchiveEntry createArchiveEntry(final File inputFile, final String entryName) throws IOException {
332 checkFinished();
333 return new TarArchiveEntry(inputFile, entryName);
334 }
335
336 @Override
337 public TarArchiveEntry createArchiveEntry(final Path inputPath, final String entryName, final LinkOption... options) throws IOException {
338 checkFinished();
339 return new TarArchiveEntry(inputPath, entryName, options);
340 }
341
342 private byte[] encodeExtendedPaxHeadersContents(final Map<String, String> headers) {
343 final StringWriter w = new StringWriter();
344 headers.forEach((k, v) -> {
345 int len = k.length() + v.length() + 3
346 + 2 ;
347 String line = len + " " + k + "=" + v + "\n";
348 int actualLength = line.getBytes(UTF_8).length;
349 while (len != actualLength) {
350
351
352
353
354
355 len = actualLength;
356 line = len + " " + k + "=" + v + "\n";
357 actualLength = line.getBytes(UTF_8).length;
358 }
359 w.write(line);
360 });
361 return w.toString().getBytes(UTF_8);
362 }
363
364 private void failForBigNumber(final String field, final long value, final long maxValue) {
365 failForBigNumber(field, value, maxValue, "");
366 }
367
368 private void failForBigNumber(final String field, final long value, final long maxValue, final String additionalMsg) {
369 if (value < 0 || value > maxValue) {
370 throw new IllegalArgumentException(field + " '" + value
371 + "' is too big ( > " + maxValue + " )." + additionalMsg);
372 }
373 }
374
375 private void failForBigNumbers(final TarArchiveEntry entry) {
376 failForBigNumber("entry size", entry.getSize(), TarConstants.MAXSIZE);
377 failForBigNumberWithPosixMessage("group id", entry.getLongGroupId(), TarConstants.MAXID);
378 failForBigNumber("last modification time", TimeUtils.toUnixTime(entry.getLastModifiedTime()), TarConstants.MAXSIZE);
379 failForBigNumber("user id", entry.getLongUserId(), TarConstants.MAXID);
380 failForBigNumber("mode", entry.getMode(), TarConstants.MAXID);
381 failForBigNumber("major device number", entry.getDevMajor(), TarConstants.MAXID);
382 failForBigNumber("minor device number", entry.getDevMinor(), TarConstants.MAXID);
383 }
384
385 private void failForBigNumberWithPosixMessage(final String field, final long value, final long maxValue) {
386 failForBigNumber(field, value, maxValue, " Use STAR or POSIX extensions to overcome this limit");
387 }
388
389
390
391
392
393
394
395
396
397 @Override
398 public void finish() throws IOException {
399 checkFinished();
400 if (haveUnclosedEntry) {
401 throw new IOException("This archive contains unclosed entries.");
402 }
403 writeEOFRecord();
404 writeEOFRecord();
405 padAsNeeded();
406 out.flush();
407 super.finish();
408 }
409
410 @Override
411 public long getBytesWritten() {
412 return countingOut.getByteCount();
413 }
414
415 @Deprecated
416 @Override
417 public int getCount() {
418 return (int) getBytesWritten();
419 }
420
421
422
423
424
425
426
427 @Deprecated
428 public int getRecordSize() {
429 return RECORD_SIZE;
430 }
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455 private boolean handleLongName(final TarArchiveEntry entry, final String name, final Map<String, String> paxHeaders, final String paxHeaderName,
456 final byte linkType, final String fieldName) throws IOException {
457 final ByteBuffer encodedName = zipEncoding.encode(name);
458 final int len = encodedName.limit() - encodedName.position();
459 if (len >= TarConstants.NAMELEN) {
460
461 if (longFileMode == LONGFILE_POSIX) {
462 paxHeaders.put(paxHeaderName, name);
463 return true;
464 }
465 if (longFileMode == LONGFILE_GNU) {
466
467
468 final TarArchiveEntry longLinkEntry = new TarArchiveEntry(TarConstants.GNU_LONGLINK, linkType);
469
470 longLinkEntry.setSize(len + 1L);
471 transferModTime(entry, longLinkEntry);
472 putArchiveEntry(longLinkEntry);
473 write(encodedName.array(), encodedName.arrayOffset(), len);
474 write(0);
475 closeArchiveEntry();
476 } else if (longFileMode != LONGFILE_TRUNCATE) {
477 throw new IllegalArgumentException(fieldName + " '" + name
478 + "' is too long ( > " + TarConstants.NAMELEN + " bytes)");
479 }
480 }
481 return false;
482 }
483
484 private void padAsNeeded() throws IOException {
485 final int start = Math.toIntExact(recordsWritten % recordsPerBlock);
486 if (start != 0) {
487 for (int i = start; i < recordsPerBlock; i++) {
488 writeEOFRecord();
489 }
490 }
491 }
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506 @Override
507 public void putArchiveEntry(final TarArchiveEntry archiveEntry) throws IOException {
508 checkFinished();
509 if (archiveEntry.isGlobalPaxHeader()) {
510 final byte[] data = encodeExtendedPaxHeadersContents(archiveEntry.getExtraPaxHeaders());
511 archiveEntry.setSize(data.length);
512 archiveEntry.writeEntryHeader(recordBuf, zipEncoding, bigNumberMode == BIGNUMBER_STAR);
513 writeRecord(recordBuf);
514 currSize = archiveEntry.getSize();
515 currBytes = 0;
516 this.haveUnclosedEntry = true;
517 write(data);
518 closeArchiveEntry();
519 } else {
520 final Map<String, String> paxHeaders = new HashMap<>();
521 final String entryName = archiveEntry.getName();
522 final boolean paxHeaderContainsPath = handleLongName(archiveEntry, entryName, paxHeaders, "path", TarConstants.LF_GNUTYPE_LONGNAME, "file name");
523 final String linkName = archiveEntry.getLinkName();
524 final boolean paxHeaderContainsLinkPath = linkName != null && !linkName.isEmpty()
525 && handleLongName(archiveEntry, linkName, paxHeaders, "linkpath", TarConstants.LF_GNUTYPE_LONGLINK, "link name");
526
527 if (bigNumberMode == BIGNUMBER_POSIX) {
528 addPaxHeadersForBigNumbers(paxHeaders, archiveEntry);
529 } else if (bigNumberMode != BIGNUMBER_STAR) {
530 failForBigNumbers(archiveEntry);
531 }
532
533 if (addPaxHeadersForNonAsciiNames && !paxHeaderContainsPath && !ASCII.canEncode(entryName)) {
534 paxHeaders.put("path", entryName);
535 }
536
537 if (addPaxHeadersForNonAsciiNames && !paxHeaderContainsLinkPath && (archiveEntry.isLink() || archiveEntry.isSymbolicLink())
538 && !ASCII.canEncode(linkName)) {
539 paxHeaders.put("linkpath", linkName);
540 }
541 paxHeaders.putAll(archiveEntry.getExtraPaxHeaders());
542
543 if (!paxHeaders.isEmpty()) {
544 writePaxHeaders(archiveEntry, entryName, paxHeaders);
545 }
546
547 archiveEntry.writeEntryHeader(recordBuf, zipEncoding, bigNumberMode == BIGNUMBER_STAR);
548 writeRecord(recordBuf);
549
550 currBytes = 0;
551
552 if (archiveEntry.isDirectory()) {
553 currSize = 0;
554 } else {
555 currSize = archiveEntry.getSize();
556 }
557 currName = entryName;
558 haveUnclosedEntry = true;
559 }
560 }
561
562
563
564
565
566
567
568 public void setAddPaxHeadersForNonAsciiNames(final boolean b) {
569 addPaxHeadersForNonAsciiNames = b;
570 }
571
572
573
574
575
576
577
578
579 public void setBigNumberMode(final int bigNumberMode) {
580 this.bigNumberMode = bigNumberMode;
581 }
582
583
584
585
586
587
588
589 public void setLongFileMode(final int longFileMode) {
590 this.longFileMode = longFileMode;
591 }
592
593
594
595
596
597
598 private boolean shouldBeReplaced(final char c) {
599 return c == 0
600 || c == '/'
601 || c == '\\';
602 }
603
604 private String stripTo7Bits(final String name) {
605 final int length = name.length();
606 final StringBuilder result = new StringBuilder(length);
607 for (int i = 0; i < length; i++) {
608 final char stripped = (char) (name.charAt(i) & 0x7F);
609 if (shouldBeReplaced(stripped)) {
610 result.append("_");
611 } else {
612 result.append(stripped);
613 }
614 }
615 return result.toString();
616 }
617
618 private void transferModTime(final TarArchiveEntry from, final TarArchiveEntry to) {
619 long fromModTimeSeconds = TimeUtils.toUnixTime(from.getLastModifiedTime());
620 if (fromModTimeSeconds < 0 || fromModTimeSeconds > TarConstants.MAXSIZE) {
621 fromModTimeSeconds = 0;
622 }
623 to.setLastModifiedTime(FileTimes.fromUnixTime(fromModTimeSeconds));
624 }
625
626
627
628
629
630
631
632
633
634
635 @Override
636 public void write(final byte[] wBuf, final int wOffset, final int numToWrite) throws IOException {
637 if (!haveUnclosedEntry) {
638 throw new IllegalStateException("No current tar entry");
639 }
640 if (currBytes + numToWrite > currSize) {
641 throw new IOException(
642 "Request to write '" + numToWrite + "' bytes exceeds size in header of '" + currSize + "' bytes for entry '" + currName + "'");
643 }
644 out.write(wBuf, wOffset, numToWrite);
645 currBytes += numToWrite;
646 }
647
648
649
650
651 private void writeEOFRecord() throws IOException {
652 writeRecord(ArrayFill.fill(recordBuf, (byte) 0));
653 }
654
655
656
657
658
659
660 void writePaxHeaders(final TarArchiveEntry entry, final String entryName, final Map<String, String> headers) throws IOException {
661 String name = "./PaxHeaders.X/" + stripTo7Bits(entryName);
662 if (name.length() >= TarConstants.NAMELEN) {
663 name = name.substring(0, TarConstants.NAMELEN - 1);
664 }
665 final TarArchiveEntry pex = new TarArchiveEntry(name, TarConstants.LF_PAX_EXTENDED_HEADER_LC);
666 transferModTime(entry, pex);
667
668 final byte[] data = encodeExtendedPaxHeadersContents(headers);
669 pex.setSize(data.length);
670 putArchiveEntry(pex);
671 write(data);
672 closeArchiveEntry();
673 }
674
675
676
677
678
679
680
681 private void writeRecord(final byte[] record) throws IOException {
682 if (record.length != RECORD_SIZE) {
683 throw new IOException("Record to write has length '" + record.length + "' which is not the record size of '" + RECORD_SIZE + "'");
684 }
685
686 out.write(record);
687 recordsWritten++;
688 }
689 }