Skip to content

Commit 913783b

Browse files
committed
TAR: Implement extraction and archiving of hardlinks.
1 parent a39a382 commit 913783b

File tree

9 files changed

+144
-22
lines changed

9 files changed

+144
-22
lines changed

src/main/java/org/codehaus/plexus/archiver/AbstractUnArchiver.java

+26-4
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ protected Logger getLogger() {
7878
*/
7979
private boolean ignorePermissions = false;
8080

81+
private boolean warnCannotHardlink = true;
82+
8183
public AbstractUnArchiver() {
8284
// no op
8385
}
@@ -278,8 +280,9 @@ protected void extractFile(
278280
String entryName,
279281
final Date entryDate,
280282
final boolean isDirectory,
283+
final boolean isSymlink,
281284
final Integer mode,
282-
String symlinkDestination,
285+
String linkDestination,
283286
final FileMapper[] fileMappers)
284287
throws IOException, ArchiverException {
285288
if (fileMappers != null) {
@@ -312,11 +315,30 @@ protected void extractFile(
312315
dirF.mkdirs();
313316
}
314317

315-
if (!StringUtils.isEmpty(symlinkDestination)) {
316-
SymlinkUtils.createSymbolicLink(targetFileName, new File(symlinkDestination));
318+
boolean doCopy = true;
319+
if (!StringUtils.isEmpty(linkDestination)) {
320+
if (isSymlink) {
321+
SymlinkUtils.createSymbolicLink(targetFileName, new File(linkDestination));
322+
doCopy = false;
323+
} else {
324+
try {
325+
Files.createLink(
326+
targetFileName.toPath(),
327+
FileUtils.resolveFile(dir, linkDestination).toPath());
328+
doCopy = false;
329+
} catch (final UnsupportedOperationException ex) {
330+
if (warnCannotHardlink) {
331+
getLogger().warn("Creating hardlinks is not supported");
332+
warnCannotHardlink = false;
333+
}
334+
// We will do a copy instead.
335+
}
336+
}
317337
} else if (isDirectory) {
318338
targetFileName.mkdirs();
319-
} else {
339+
doCopy = false;
340+
}
341+
if (doCopy) {
320342
try (OutputStream out = Files.newOutputStream(targetFileName.toPath())) {
321343
IOUtil.copy(compressedInputStream, out);
322344
}

src/main/java/org/codehaus/plexus/archiver/tar/TarArchiver.java

+53-10
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
import java.io.InputStream;
2424
import java.io.OutputStream;
2525
import java.nio.file.Files;
26+
import java.nio.file.Path;
27+
import java.nio.file.attribute.BasicFileAttributeView;
28+
import java.util.HashMap;
29+
import java.util.Map;
2630
import java.util.zip.GZIPOutputStream;
2731

2832
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
@@ -39,6 +43,7 @@
3943
import org.codehaus.plexus.archiver.util.Streams;
4044
import org.codehaus.plexus.components.io.attributes.PlexusIoResourceAttributes;
4145
import org.codehaus.plexus.components.io.functions.SymlinkDestinationSupplier;
46+
import org.codehaus.plexus.components.io.resources.PlexusIoFileResource;
4247
import org.codehaus.plexus.components.io.resources.PlexusIoResource;
4348
import org.codehaus.plexus.util.IOUtil;
4449
import org.codehaus.plexus.util.StringUtils;
@@ -65,6 +70,8 @@ public class TarArchiver extends AbstractArchiver {
6570

6671
private TarArchiveOutputStream tOut;
6772

73+
private final Map<Object, String> seenFiles = new HashMap<>(10);
74+
6875
/**
6976
* Set how to handle long files, those with a path&gt;100 chars.
7077
* Optional, default=warn.
@@ -177,7 +184,8 @@ protected void tarFile(ArchiveEntry entry, TarArchiveOutputStream tOut, String v
177184
return;
178185
}
179186

180-
if (entry.getResource().isDirectory() && !vPath.endsWith("/")) {
187+
final PlexusIoResource ioResource = entry.getResource();
188+
if (ioResource.isDirectory() && !vPath.endsWith("/")) {
181189
vPath += "/";
182190
}
183191

@@ -194,7 +202,7 @@ protected void tarFile(ArchiveEntry entry, TarArchiveOutputStream tOut, String v
194202
InputStream fIn = null;
195203

196204
try {
197-
TarArchiveEntry te;
205+
TarArchiveEntry te = null;
198206
if (!longFileMode.isGnuMode()
199207
&& pathLength >= org.apache.commons.compress.archivers.tar.TarConstants.NAMELEN) {
200208
int maxPosixPathLen = org.apache.commons.compress.archivers.tar.TarConstants.NAMELEN
@@ -233,18 +241,43 @@ protected void tarFile(ArchiveEntry entry, TarArchiveOutputStream tOut, String v
233241
}
234242
}
235243

244+
boolean doCopy = true;
236245
if (entry.getType() == ArchiveEntry.SYMLINK) {
237-
final SymlinkDestinationSupplier plexusIoSymlinkResource =
238-
(SymlinkDestinationSupplier) entry.getResource();
246+
final SymlinkDestinationSupplier plexusIoSymlinkResource = (SymlinkDestinationSupplier) ioResource;
239247

240248
te = new TarArchiveEntry(vPath, TarArchiveEntry.LF_SYMLINK);
241249
te.setLinkName(plexusIoSymlinkResource.getSymlinkDestination());
242-
} else {
250+
doCopy = false;
251+
} else if (options.getPreserveHardLinks()
252+
&& ioResource.isFile()
253+
&& ioResource instanceof PlexusIoFileResource) {
254+
final PlexusIoFileResource fileResource = (PlexusIoFileResource) ioResource;
255+
final Path file = fileResource.getFile().toPath();
256+
if (Files.exists(file)) {
257+
final BasicFileAttributeView fileAttributeView =
258+
Files.getFileAttributeView(file, BasicFileAttributeView.class);
259+
if (fileAttributeView != null) {
260+
final Object fileKey =
261+
fileAttributeView.readAttributes().fileKey();
262+
if (fileKey != null) {
263+
final String seenFile = this.seenFiles.get(fileKey);
264+
if (seenFile != null) {
265+
te = new TarArchiveEntry(vPath, TarArchiveEntry.LF_LINK);
266+
te.setLinkName(seenFile);
267+
doCopy = false;
268+
} else {
269+
this.seenFiles.put(fileKey, vPath);
270+
}
271+
}
272+
}
273+
}
274+
}
275+
if (te == null) {
243276
te = new TarArchiveEntry(vPath);
244277
}
245278

246279
if (getLastModifiedTime() == null) {
247-
long teLastModified = entry.getResource().getLastModified();
280+
long teLastModified = ioResource.getLastModified();
248281
te.setModTime(
249282
teLastModified == PlexusIoResource.UNKNOWN_MODIFICATION_DATE
250283
? System.currentTimeMillis()
@@ -253,11 +286,11 @@ protected void tarFile(ArchiveEntry entry, TarArchiveOutputStream tOut, String v
253286
te.setModTime(getLastModifiedTime().toMillis());
254287
}
255288

256-
if (entry.getType() == ArchiveEntry.SYMLINK) {
289+
if (!doCopy) {
257290
te.setSize(0);
258291

259-
} else if (!entry.getResource().isDirectory()) {
260-
final long size = entry.getResource().getSize();
292+
} else if (!ioResource.isDirectory()) {
293+
final long size = ioResource.getSize();
261294
te.setSize(size == PlexusIoResource.UNKNOWN_RESOURCE_SIZE ? 0 : size);
262295
}
263296
te.setMode(entry.getMode());
@@ -289,7 +322,7 @@ protected void tarFile(ArchiveEntry entry, TarArchiveOutputStream tOut, String v
289322
tOut.putArchiveEntry(te);
290323

291324
try {
292-
if (entry.getResource().isFile() && !(entry.getType() == ArchiveEntry.SYMLINK)) {
325+
if (ioResource.isFile() && doCopy) {
293326
fIn = entry.getInputStream();
294327

295328
Streams.copyFullyDontCloseOutput(fIn, tOut, "xAR");
@@ -320,6 +353,8 @@ public class TarOptions {
320353

321354
private boolean preserveLeadingSlashes = false;
322355

356+
private boolean preserveHardLinks = true;
357+
323358
/**
324359
* The username for the tar entry
325360
* This is not the same as the UID.
@@ -405,6 +440,14 @@ public boolean getPreserveLeadingSlashes() {
405440
public void setPreserveLeadingSlashes(boolean preserveLeadingSlashes) {
406441
this.preserveLeadingSlashes = preserveLeadingSlashes;
407442
}
443+
444+
public boolean getPreserveHardLinks() {
445+
return preserveHardLinks;
446+
}
447+
448+
public void setPreserveHardLinks(boolean preserveHardLinks) {
449+
this.preserveHardLinks = preserveHardLinks;
450+
}
408451
}
409452

410453
/**

src/main/java/org/codehaus/plexus/archiver/tar/TarUnArchiver.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -99,14 +99,15 @@ protected void execute(File sourceFile, File destDirectory, FileMapper[] fileMap
9999
while ((te = tis.getNextTarEntry()) != null) {
100100
TarResource fileInfo = new TarResource(tarFile, te);
101101
if (isSelected(te.getName(), fileInfo)) {
102-
final String symlinkDestination = te.isSymbolicLink() ? te.getLinkName() : null;
102+
final String symlinkDestination = te.isSymbolicLink() || te.isLink() ? te.getLinkName() : null;
103103
extractFile(
104104
sourceFile,
105105
destDirectory,
106106
tis,
107107
te.getName(),
108108
te.getModTime(),
109109
te.isDirectory(),
110+
te.isSymbolicLink(),
110111
te.getMode() != 0 ? te.getMode() : null,
111112
symlinkDestination,
112113
fileMappers);

src/main/java/org/codehaus/plexus/archiver/zip/AbstractZipUnArchiver.java

+1
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ protected void execute(final String path, final File outputDirectory) throws Arc
176176
ze.getName(),
177177
new Date(ze.getTime()),
178178
ze.isDirectory(),
179+
ze.isUnixSymlink(),
179180
ze.getUnixMode() != 0 ? ze.getUnixMode() : null,
180181
resolveSymlink(zipFile, ze),
181182
getFileMappers());

src/test/java/org/codehaus/plexus/archiver/AbstractUnArchiverTest.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public void shouldThrowExceptionBecauseRewrittenPathIsOutOfDirectory(@TempDir Fi
7474
Exception exception = assertThrows(
7575
ArchiverException.class,
7676
() -> abstractUnArchiver.extractFile(
77-
null, targetFolder, null, "ENTRYNAME", null, false, null, null, fileMappers));
77+
null, targetFolder, null, "ENTRYNAME", null, false, false, null, null, fileMappers));
7878
// then
7979
// ArchiverException is thrown providing the rewritten path
8080
assertEquals(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package org.codehaus.plexus.archiver;
2+
3+
import java.io.File;
4+
import java.nio.file.Files;
5+
6+
import org.codehaus.plexus.archiver.tar.TarArchiver;
7+
import org.codehaus.plexus.archiver.tar.TarLongFileMode;
8+
import org.codehaus.plexus.archiver.tar.TarUnArchiver;
9+
import org.junit.jupiter.api.Test;
10+
import org.junit.jupiter.api.condition.DisabledOnOs;
11+
import org.junit.jupiter.api.condition.OS;
12+
13+
import static org.junit.jupiter.api.Assertions.assertTrue;
14+
15+
/**
16+
* @author Kristian Rosenvold
17+
*/
18+
public class HardlinkTest extends TestSupport {
19+
20+
@Test
21+
@DisabledOnOs(OS.WINDOWS)
22+
public void testHardlinkTar() throws Exception {
23+
// Extract test files
24+
final File archiveFile = getTestFile("src/test/resources/hardlinks/hardlinks.tar");
25+
File output = getTestFile("target/output/untaredHardlinks");
26+
output.mkdirs();
27+
TarUnArchiver unarchiver = (TarUnArchiver) lookup(UnArchiver.class, "tar");
28+
unarchiver.setSourceFile(archiveFile);
29+
unarchiver.setDestFile(output);
30+
unarchiver.extract();
31+
// Check that we have hardlinks
32+
assertTrue(Files.isSameFile(
33+
output.toPath().resolve("fileR.txt"), output.toPath().resolve("hardlink")));
34+
35+
// Archive the extracted hardlinks to new archive
36+
TarArchiver archiver = (TarArchiver) lookup(Archiver.class, "tar");
37+
archiver.setLongfile(TarLongFileMode.posix);
38+
archiver.addDirectory(output);
39+
final File testFile = getTestFile("target/output/untaredHardlinks2.tar");
40+
archiver.setDestFile(testFile);
41+
archiver.createArchive();
42+
43+
// Check that our created archive actually contains hardlinks when extracted
44+
unarchiver = (TarUnArchiver) lookup(UnArchiver.class, "tar");
45+
output = getTestFile("target/output/untaredHardlinks2");
46+
output.mkdirs();
47+
unarchiver.setSourceFile(testFile);
48+
unarchiver.setDestFile(output);
49+
unarchiver.extract();
50+
assertTrue(Files.isSameFile(
51+
output.toPath().resolve("fileR.txt"), output.toPath().resolve("hardlink")));
52+
}
53+
}

src/test/java/org/codehaus/plexus/archiver/tar/TarFileTest.java

+5-5
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
import java.io.IOException;
55
import java.io.InputStream;
66
import java.nio.file.Files;
7-
import java.util.Arrays;
87
import java.util.Enumeration;
98

9+
import org.apache.commons.compress.archivers.ArchiveEntry;
1010
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
1111
import org.codehaus.plexus.archiver.Archiver;
1212
import org.codehaus.plexus.archiver.TestSupport;
@@ -18,7 +18,7 @@
1818
import org.junit.jupiter.api.Test;
1919

2020
import static org.codehaus.plexus.components.io.resources.ResourceFactory.createResource;
21-
import static org.junit.jupiter.api.Assertions.assertTrue;
21+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
2222

2323
/**
2424
* Test case for {@link TarFile}.
@@ -92,15 +92,15 @@ private void testTarFile(Compressor compressor, String extension, TarFileCreator
9292
}
9393
final TarFile tarFile = tarFileCreator.newTarFile(file);
9494

95-
for (Enumeration en = tarFile.getEntries(); en.hasMoreElements(); ) {
95+
for (Enumeration<ArchiveEntry> en = tarFile.getEntries(); en.hasMoreElements(); ) {
9696
final TarArchiveEntry te = (TarArchiveEntry) en.nextElement();
97-
if (te.isDirectory() || te.isSymbolicLink()) {
97+
if (te.isDirectory() || te.isSymbolicLink() || te.isLink()) {
9898
continue;
9999
}
100100
final File teFile = new File("src", te.getName());
101101
final InputStream teStream = tarFile.getInputStream(te);
102102
final InputStream fileStream = Files.newInputStream(teFile.toPath());
103-
assertTrue(Arrays.equals(IOUtil.toByteArray(teStream), IOUtil.toByteArray(fileStream)));
103+
assertArrayEquals(IOUtil.toByteArray(teStream), IOUtil.toByteArray(fileStream));
104104
teStream.close();
105105
fileStream.close();
106106
}
10 KB
Binary file not shown.

src/test/resources/symlinks/regen.sh

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,6 @@ rm symlinks.tar
33
cd src
44
zip --symlinks ../symlinks.zip file* targetDir sym*
55
tar -cvf ../symlinks.tar file* targetDir sym*
6-
6+
rm hardlink
7+
ln fileR.txt hardlink
8+
tar -cvf ../../hardlinks/hardlinks.tar fileR.txt hardlink

0 commit comments

Comments
 (0)