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