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