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
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.codehaus.plexus.components.io.functions;

/*
* Copyright 2026 The plexus developers.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import java.io.IOException;

/**
* Optional identity for resources whose contents can share one hard-link payload.
* Implementations must return null when transformation or a replacement content
* supplier invalidates that relationship. Equal identities must denote the same
* content within a stable source, not merely equal bytes from unrelated files.
* Consumers must also compare the metadata they intend to write.
*
* @since 3.7.1
*/
public interface HardLinkIdentitySupplier {
/**
* Returns a source-scoped identity suitable for equality comparisons.
* Identities must only be cached for the duration of one archive operation.
*
* @return the content identity, or null when preservation cannot be guaranteed
* @throws IOException if source attributes cannot be read
*/
Object getHardLinkIdentity() throws IOException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,12 @@
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.DeferredFileOutputStream;
import org.codehaus.plexus.components.io.functions.ContentSupplier;
import org.codehaus.plexus.components.io.functions.HardLinkIdentitySupplier;
import org.codehaus.plexus.components.io.functions.NameSupplier;
import org.codehaus.plexus.components.io.functions.SizeSupplier;
import org.codehaus.plexus.components.io.resources.proxy.ProxyFactory;

class Deferred implements ContentSupplier, NameSupplier, SizeSupplier {
class Deferred implements ContentSupplier, NameSupplier, SizeSupplier, HardLinkIdentitySupplier {
final DeferredFileOutputStream dfos;

final PlexusIoResource resource;
Expand Down Expand Up @@ -87,6 +88,19 @@ public String getName() {
return owner.getName(resource);
}

/**
* Preserves source identity only when this wrapper has not transformed the contents.
*
* @return the source identity, or null after transformation
* @throws IOException if the source identity cannot be read
*/
@Override
public Object getHardLinkIdentity() throws IOException {
return dfos == null && resource instanceof HardLinkIdentitySupplier
? ((HardLinkIdentitySupplier) resource).getHardLinkIdentity()
: null;
}

public PlexusIoResource asResource() {
return ProxyFactory.createProxy(resource, Deferred.this);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.util.Arrays;

import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.DeferredFileOutputStream;
Expand All @@ -34,6 +37,7 @@
import org.codehaus.plexus.components.io.attributes.PlexusIoResourceAttributes;
import org.codehaus.plexus.components.io.functions.ContentSupplier;
import org.codehaus.plexus.components.io.functions.FileSupplier;
import org.codehaus.plexus.components.io.functions.HardLinkIdentitySupplier;
import org.codehaus.plexus.components.io.functions.InputStreamTransformer;
import org.codehaus.plexus.components.io.functions.ResourceAttributeSupplier;

Expand All @@ -42,7 +46,8 @@
/**
* Implementation of {@link PlexusIoResource} for files.
*/
public class PlexusIoFileResource extends AbstractPlexusIoResource implements ResourceAttributeSupplier, FileSupplier {
public class PlexusIoFileResource extends AbstractPlexusIoResource
implements ResourceAttributeSupplier, FileSupplier, HardLinkIdentitySupplier {

@Nonnull
private final File file;
Expand All @@ -55,6 +60,8 @@ public class PlexusIoFileResource extends AbstractPlexusIoResource implements Re

private final ContentSupplier contentSupplier;

private final boolean originalContent;

private final DeferredFileOutputStream dfos;

protected PlexusIoFileResource(@Nonnull File file, @Nonnull String name, @Nonnull PlexusIoResourceAttributes attrs)
Expand Down Expand Up @@ -98,6 +105,8 @@ protected PlexusIoFileResource(@Nonnull File file, @Nonnull String name, @Nonnul
boolean hasTransformer = streamTransformer != null && streamTransformer != identityTransformer;
InputStreamTransformer transToUse = streamTransformer != null ? streamTransformer : identityTransformer;

originalContent = contentSupplier == null && !hasTransformer;

dfos = hasTransformer && file.isFile() ? asDeferredStream(this.contentSupplier, transToUse, this) : null;
}

Expand Down Expand Up @@ -205,6 +214,28 @@ public boolean isSymbolicLink() {
return getAttributes().isSymbolicLink();
}

/**
* Identifies an untransformed regular file without following symbolic links.
* Subclasses that change content must explicitly supply their own guarantee.
*
* @return a filesystem-scoped file identity, or null for unknown content
* @throws IOException if the file attributes cannot be read
*/
@Override
public Object getHardLinkIdentity() throws IOException {
// A backing file alone does not describe custom suppliers or transformed bytes.
if (!originalContent || getClass() != PlexusIoFileResource.class) {
return null;
}
BasicFileAttributes attrs =
Files.readAttributes(file.toPath(), BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
if (!attrs.isRegularFile() || attrs.fileKey() == null) {
return null;
}
// Include the observed content version so a changed source cannot reuse an old payload.
return Arrays.asList(file.toPath().getFileSystem(), attrs.fileKey(), attrs.size(), attrs.lastModifiedTime());
}

protected DeferredFileOutputStream getDfos() {
return dfos;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.util.List;

import org.codehaus.plexus.components.io.functions.FileSupplier;
import org.codehaus.plexus.components.io.functions.HardLinkIdentitySupplier;
import org.codehaus.plexus.components.io.functions.ResourceAttributeSupplier;
import org.codehaus.plexus.components.io.functions.SymlinkDestinationSupplier;
import org.codehaus.plexus.components.io.resources.PlexusIoResource;
Expand All @@ -31,6 +32,9 @@ public static PlexusIoResource createProxy(@Nonnull PlexusIoResource target, Obj

List<Class> interfaces = new ArrayList<>();
interfaces.add(PlexusIoResource.class);
if (target instanceof HardLinkIdentitySupplier || alternateSupplier instanceof HardLinkIdentitySupplier) {
interfaces.add(HardLinkIdentitySupplier.class);
}
if (target instanceof SymlinkDestinationSupplier) interfaces.add(SymlinkDestinationSupplier.class);
if (target instanceof FileSupplier) interfaces.add(FileSupplier.class);
if (target instanceof ResourceAttributeSupplier) interfaces.add(ResourceAttributeSupplier.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.lang.reflect.Method;

import org.codehaus.plexus.components.io.functions.ContentSupplier;
import org.codehaus.plexus.components.io.functions.HardLinkIdentitySupplier;
import org.codehaus.plexus.components.io.functions.NameSupplier;
import org.codehaus.plexus.components.io.functions.ResourceAttributeSupplier;
import org.codehaus.plexus.components.io.functions.SizeSupplier;
Expand All @@ -30,6 +31,8 @@
class ResourceInvocationHandler implements InvocationHandler {
private final PlexusIoResource testImpl;

private final HardLinkIdentitySupplier hardLinkIdentitySupplier;

private final ContentSupplier contentSupplier;
private final NameSupplier nameSupplier;
private final SizeSupplier sizeSupplier;
Expand All @@ -38,6 +41,7 @@ class ResourceInvocationHandler implements InvocationHandler {

public ResourceInvocationHandler(@Nonnull PlexusIoResource target, Object alternativeHandler) {
this.testImpl = target;
this.hardLinkIdentitySupplier = asOrNull(alternativeHandler, HardLinkIdentitySupplier.class);
this.contentSupplier = asOrNull(alternativeHandler, ContentSupplier.class);
this.nameSupplier = asOrNull(alternativeHandler, NameSupplier.class);
this.sizeSupplier = asOrNull(alternativeHandler, SizeSupplier.class);
Expand All @@ -53,6 +57,15 @@ private static <T> T asOrNull(Object instance, Class<T> clazz) {

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String name = method.getName();
if ("getHardLinkIdentity".equals(name)) {
// Replacing contents invalidates identity unless the wrapper explicitly preserves it.
if (hardLinkIdentitySupplier != null) {
return hardLinkIdentitySupplier.getHardLinkIdentity();
}
return contentSupplier == null && sizeSupplier == null && testImpl instanceof HardLinkIdentitySupplier
? ((HardLinkIdentitySupplier) testImpl).getHardLinkIdentity()
: null;
}
if (contentSupplier != null && "getContents".equals(name)) {
return contentSupplier.getContents();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Copyright 2026 The plexus developers.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.codehaus.plexus.components.io.resources;

import java.io.ByteArrayInputStream;
import java.nio.file.Files;
import java.nio.file.Path;

import org.codehaus.plexus.components.io.functions.ContentSupplier;
import org.codehaus.plexus.components.io.functions.HardLinkIdentitySupplier;
import org.codehaus.plexus.components.io.functions.NameSupplier;
import org.codehaus.plexus.components.io.resources.proxy.ProxyFactory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

class HardLinkIdentityTest {
@TempDir
Path temp;

/** Checks filesystem identity and invalidation when effective content changes. */
@Test
void identifiesOnlyOriginalContent() throws Exception {
Path first = Files.write(temp.resolve("first"), new byte[] {1});
Path second = temp.resolve("second");
try {
Files.createLink(second, first);
} catch (java.io.IOException | UnsupportedOperationException e) {
assumeTrue(false, "Hard links unavailable: " + e);
}
PlexusIoResource a = ResourceFactory.createResource(first.toFile());
PlexusIoResource b = ResourceFactory.createResource(second.toFile());
Object identity = ((HardLinkIdentitySupplier) a).getHardLinkIdentity();
assumeTrue(identity != null, "Filesystem does not expose file keys");
assertEquals(identity, ((HardLinkIdentitySupplier) b).getHardLinkIdentity());
PlexusIoResource transformed = ResourceFactory.createResource(
first.toFile(), (resource, input) -> new ByteArrayInputStream(new byte[] {2}));
assertNull(((HardLinkIdentitySupplier) transformed).getHardLinkIdentity());
PlexusIoResource supplied = ResourceFactory.createResource(
first.toFile(),
"custom",
() -> new ByteArrayInputStream(new byte[] {3}),
(org.codehaus.plexus.components.io.functions.InputStreamTransformer) null);
assertNull(((HardLinkIdentitySupplier) supplied).getHardLinkIdentity());
}

/** Transparent renaming keeps identity while a replacement stream invalidates it. */
@Test
void proxiesRespectContentOverrides() throws Exception {
Path file = Files.write(temp.resolve("file"), new byte[] {1});
PlexusIoResource original = ResourceFactory.createResource(file.toFile());
Object identity = ((HardLinkIdentitySupplier) original).getHardLinkIdentity();
PlexusIoResource renamed = ProxyFactory.createProxy(original, (NameSupplier) () -> "renamed");
assertEquals(identity, ((HardLinkIdentitySupplier) renamed).getHardLinkIdentity());
PlexusIoResource replaced =
ProxyFactory.createProxy(original, (ContentSupplier) () -> new ByteArrayInputStream(new byte[] {2}));
assertNull(((HardLinkIdentitySupplier) replaced).getHardLinkIdentity());
}

/** Collection resolution must explicitly preserve or discard the source guarantee. */
@Test
void deferredResourcesRespectTransformers() throws Exception {
Path file = Files.write(temp.resolve("file"), new byte[] {1});
PlexusIoResource original = ResourceFactory.createResource(file.toFile());
Object identity = ((HardLinkIdentitySupplier) original).getHardLinkIdentity();
PlexusIoResourceCollection owner = new PlexusIoFileResourceCollection();
assertEquals(
identity,
((HardLinkIdentitySupplier) new Deferred(original, owner, false).asResource()).getHardLinkIdentity());
assertNull(((HardLinkIdentitySupplier) new Deferred(original, owner, true).asResource()).getHardLinkIdentity());
}
}