Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
<dependency>
<groupId>org.codehaus.plexus</groupId>
<artifactId>plexus-io</artifactId>
<version>3.7.0</version>
<version>3.7.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
Expand Down
58 changes: 29 additions & 29 deletions src/main/java/org/codehaus/plexus/archiver/AbstractArchiver.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.attribute.FileTime;
Expand Down Expand Up @@ -473,6 +472,9 @@ public boolean hasNext() {

try {
ioResourceIter = currentResourceCollection.resources.getResources();
// Register ownership before reading, since iteration can fail before
// exhaustion.
addCloseable(ioResourceIter);
} catch (final IOException e) {
throw new ArchiverException(e.getMessage(), e);
}
Expand All @@ -487,12 +489,6 @@ public boolean hasNext() {
final PlexusIoResource resource = (PlexusIoResource) ioResourceIter.next();
nextEntry = asArchiveEntry(currentResourceCollection, resource);
} else {
// this will leak handles in the IO iterator if the iterator is not fully consumed.
// alternately we'd have to make this method return a Closeable iterator back
// to the client and ditch the whole issue onto the client.
// this does not really make any sense either, might equally well change the
// api into something that is not broken by design.
addCloseable(ioResourceIter);
ioResourceIter = null;
}
}
Expand Down Expand Up @@ -552,20 +548,19 @@ private String normalizedForDuplicateCheck(ArchiveEntry entry) {
};
}

/** Reaches the owning collection through permission and proxy wrappers before releasing its resources. */
private static void closeIfCloseable(Object resource) throws IOException {
if (resource instanceof AddedResourceCollection collection) {
resource = collection.resources;
}
while (resource instanceof PlexusIoProxyResourceCollection collection) {
resource = collection.getSrc();
}
if (resource instanceof Closeable closeable) {
closeable.close();
}
}

private static void closeQuietlyIfCloseable(Object resource) {
try {
closeIfCloseable(resource);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}

@Override
public File getDestFile() {
return destFile;
Expand Down Expand Up @@ -832,25 +827,30 @@ private void addCloseable(Object maybeCloseable) {
}
}

private void closeIterators() {
for (Closeable closeable : closeables) {
closeQuietlyIfCloseable(closeable);
}
}

protected abstract void close() throws IOException;

/** Releases iterators and owning collections on success or failure, attempting every registered close. */
protected void cleanUp() throws IOException {
closeIterators();

for (Object resource : resources) {
if (resource instanceof PlexusIoProxyResourceCollection collection) {
resource = collection.getSrc();
List<Object> pending = new ArrayList<>(closeables);
pending.addAll(resources);
// Detach ownership before closing so a failure cannot retain readers across archive operations.
closeables.clear();
resources.clear();
IOException failure = null;
for (Object resource : pending) {
try {
closeIfCloseable(resource);
} catch (IOException e) {
if (failure == null) {
failure = e;
} else if (failure != e) {
failure.addSuppressed(e);
}
}

closeIfCloseable(resource);
}
resources.clear();
if (failure != null) {
throw failure;
}
}

protected abstract void execute() throws ArchiverException, IOException;
Expand Down
115 changes: 112 additions & 3 deletions src/main/java/org/codehaus/plexus/archiver/tar/TarArchiver.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,16 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.GZIPOutputStream;

import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import org.apache.commons.compress.archivers.tar.TarConstants;
import org.apache.commons.compress.compressors.bzip2.BZip2CompressorOutputStream;
import org.apache.commons.compress.compressors.snappy.FramedSnappyCompressorOutputStream;
import org.apache.commons.compress.compressors.xz.XZCompressorOutputStream;
Expand All @@ -38,6 +44,7 @@
import org.codehaus.plexus.archiver.util.ResourceUtils;
import org.codehaus.plexus.archiver.util.Streams;
import org.codehaus.plexus.components.io.attributes.PlexusIoResourceAttributes;
import org.codehaus.plexus.components.io.functions.HardLinkIdentitySupplier;
import org.codehaus.plexus.components.io.functions.SymlinkDestinationSupplier;
import org.codehaus.plexus.components.io.resources.PlexusIoResource;
import org.codehaus.plexus.util.IOUtil;
Expand All @@ -64,6 +71,26 @@ public class TarArchiver extends AbstractArchiver {

private TarArchiveOutputStream tOut;

private boolean preserveHardLinks;
private final Map<HardLinkKey, String> hardLinkTargets = new HashMap<>();
private final Map<String, HardLinkKey> writtenNames = new HashMap<>();

/**
* Enables preservation of known, untransformed hard-link identities; default false.
* Unknown identities and entries with different output metadata are written in full.
* Like GNU tar, preservation continues when output paths traverse archived symbolic links;
* extraction then links to the target path's current contents.
* @param preserveHardLinks whether eligible aliases share one TAR payload
*/
public void setPreserveHardLinks(boolean preserveHardLinks) {
this.preserveHardLinks = preserveHardLinks;
}

/** Returns whether hard-link preservation was explicitly enabled. */
public boolean isPreserveHardLinks() {
return preserveHardLinks;
}

/**
* Set how to handle long files, those with a path&gt;100 chars.
* Optional, default=warn.
Expand Down Expand Up @@ -102,6 +129,9 @@ public void setCompression(TarCompressionMethod mode) {

@Override
protected void execute() throws ArchiverException, IOException {
// Identity and target names are meaningful only within the archive currently being written.
hardLinkTargets.clear();
writtenNames.clear();
if (!checkForced()) {
return;
}
Expand Down Expand Up @@ -285,25 +315,102 @@ protected void tarFile(ArchiveEntry entry, TarArchiveOutputStream tOut, String v
te.setGroupId(groupId);
}

HardLinkKey identity = hardLinkKey(entry, te);
String outputName = te.getName();
String outputKey = normalizedOutputName(outputName);
// Even an ineligible link target can replace an earlier destination through a contained parent path.
HardLinkKey replaced = writtenNames.remove(outputKey);
if (replaced != null) {
hardLinkTargets.remove(replaced);
}
String target = identity == null ? null : hardLinkTargets.get(identity);
if (target != null) {
te = hardLinkEntry(te, target);
}
tOut.putArchiveEntry(te);

try {
if (entry.getResource().isFile() && !(entry.getType() == ArchiveEntry.SYMLINK)) {
if (entry.getResource().isFile() && !te.isSymbolicLink() && !te.isLink()) {
fIn = entry.getInputStream();

Streams.copyFullyDontCloseOutput(fIn, tOut, "xAR");
}

} catch (Throwable e) {
getLogger().warn("When creating tar entry", e);
} finally {
tOut.closeArchiveEntry();
}
// Failed or omitted entries must never become targets of later link headers.
if (identity != null && target == null) {
hardLinkTargets.put(identity, outputName);
writtenNames.put(outputKey, identity);
}
} finally {
IOUtil.close(fIn);
}
}

/**
* Normalizes destination paths for target invalidation, including contained parent components.
* Emitted member names and the stricter eligibility rules for hard-link targets remain separate.
*/
private static String normalizedOutputName(String name) {
Deque<String> components = new ArrayDeque<>();
for (String component : name.split("/")) {
if (component.equals("..")
&& !components.isEmpty()
&& !components.peekLast().equals("..")) {
components.removeLast();
} else if (!component.isEmpty() && !component.equals(".")) {
components.addLast(component);
}
}
return String.join("/", components);
}

/** Returns an identity only when preservation is enabled and the resource guarantees its bytes. */
private HardLinkKey hardLinkKey(ArchiveEntry entry, TarArchiveEntry header) throws IOException {
String name;
if (!preserveHardLinks
|| longFileMode.isTruncateMode()
|| entry.getType() != ArchiveEntry.FILE
|| (name = header.getName()).startsWith("/")
|| name.contains("\\")
|| name.contains(":")
|| Arrays.asList(name.split("/")).contains("..")
|| !(entry.getResource() instanceof HardLinkIdentitySupplier supplier)) {
return null;
}
Object identity = supplier.getHardLinkIdentity();
return identity == null
? null
: new HardLinkKey(
identity,
header.getMode(),
header.getLongUserId(),
header.getLongGroupId(),
header.getUserName(),
header.getGroupName(),
header.getModTime().getTime(),
header.getSize());
}

/** Constructs a zero-payload link while retaining the effective metadata already calculated. */
private TarArchiveEntry hardLinkEntry(TarArchiveEntry original, String target) {
TarArchiveEntry link = new TarArchiveEntry(original.getName(), TarConstants.LF_LINK);
link.setLinkName(target);
link.setMode(original.getMode());
link.setUserId(original.getLongUserId());
link.setGroupId(original.getLongGroupId());
link.setUserName(original.getUserName());
link.setGroupName(original.getGroupName());
link.setModTime(original.getModTime());
return link;
}

/** Includes output metadata because one inode cannot preserve conflicting attributes. */
private record HardLinkKey(
Object identity, int mode, long uid, long gid, String user, String group, long modified, long size) {}

/**
* Valid Modes for Compression attribute to Tar Task
*/
Expand Down Expand Up @@ -442,6 +549,8 @@ public boolean isSupportingForced() {

@Override
protected void cleanUp() throws IOException {
hardLinkTargets.clear();
writtenNames.clear();
super.cleanUp();
if (this.tOut != null) {
this.tOut.close();
Expand Down
Loading
Loading