Merge branch '4.1.x'

Closes gh-51582
This commit is contained in:
Andy Wilkinson
2026-09-04 11:47:04 +01:00
2 changed files with 51 additions and 15 deletions
@@ -49,6 +49,7 @@ import org.springframework.util.StreamUtils;
*
* @author Scott Frederick
* @author Phillip Webb
* @author Junhwan Choi
*/
final class ImageBuildpack implements Buildpack {
@@ -137,22 +138,17 @@ final class ImageBuildpack implements Buildpack {
tarArchive.writeTo(out);
}
Path layerFile = Files.createTempFile("create-builder-scratch-", null);
try (TarArchiveOutputStream out = new TarArchiveOutputStream(Files.newOutputStream(layerFile))) {
try (TarArchiveInputStream in = new TarArchiveInputStream(Files.newInputStream(sourceTarFile))) {
out.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX);
TarArchiveEntry entry = in.getNextEntry();
while (entry != null) {
String entryName = entry.getName();
Path entryPath = Path.of(entryName);
Assert.state(entryPath.toAbsolutePath().equals(entryPath.toAbsolutePath().normalize()),
() -> "Malformed zip entry name '%s'".formatted(entryName));
out.putArchiveEntry(entry);
StreamUtils.copy(in, out);
out.closeArchiveEntry();
entry = in.getNextEntry();
}
out.finish();
try {
writeLayerFile(layerFile, sourceTarFile);
}
catch (IOException | RuntimeException ex) {
try {
Files.deleteIfExists(layerFile);
}
catch (IOException suppressed) {
ex.addSuppressed(suppressed);
}
throw ex;
}
return layerFile;
}
@@ -161,6 +157,26 @@ final class ImageBuildpack implements Buildpack {
}
}
private void writeLayerFile(Path layerFile, Path sourceTarFile) throws IOException {
try (TarArchiveOutputStream out = new TarArchiveOutputStream(Files.newOutputStream(layerFile))) {
try (TarArchiveInputStream in = new TarArchiveInputStream(Files.newInputStream(sourceTarFile))) {
out.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX);
TarArchiveEntry entry = in.getNextEntry();
while (entry != null) {
String entryName = entry.getName();
Path entryPath = Path.of(entryName);
Assert.state(entryPath.toAbsolutePath().equals(entryPath.toAbsolutePath().normalize()),
() -> "Malformed zip entry name '%s'".formatted(entryName));
out.putArchiveEntry(entry);
StreamUtils.copy(in, out);
out.closeArchiveEntry();
entry = in.getNextEntry();
}
out.finish();
}
}
}
void apply(IOConsumer<Layer> layers) throws IOException {
for (Path path : this.layerFiles) {
layers.accept(Layer.fromTarArchive((out) -> {
@@ -61,6 +61,7 @@ import static org.mockito.Mockito.mock;
*
* @author Scott Frederick
* @author Phillip Webb
* @author Junhwan Choi
*/
class ImageBuildpackTests extends AbstractJsonTests {
@@ -200,6 +201,25 @@ class ImageBuildpackTests extends AbstractJsonTests {
.withMessage("Malformed zip entry name '../cnb/'");
}
@Test
void resolveWhenLayerCreationFailsDeletesLayerTempFiles() throws Exception {
File tempDir = new File(System.getProperty("java.io.tmpdir"));
Set<String> tempsBefore = listTempFileNames(tempDir, "create-builder-scratch-");
Image image = Image.of(getContent("buildpack-image.json"));
ImageReference imageReference = ImageReference.of("example/buildpack1:latest");
BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class);
given(resolverContext.getBuildpackLayersMetadata()).willReturn(BuildpackLayersMetadata.fromJson("{}"));
given(resolverContext.fetchImage(eq(imageReference), eq(ImageType.BUILDPACK))).willReturn(image);
willAnswer((invocation) -> withMockLayers(invocation, "..")).given(resolverContext)
.exportImageLayers(eq(imageReference), any());
BuildpackReference reference = BuildpackReference.of("example/buildpack1");
assertThatIllegalStateException().isThrownBy(() -> ImageBuildpack.resolve(resolverContext, reference))
.withMessage("Malformed zip entry name '../cnb/'");
Set<String> tempsAfter = new HashSet<>(listTempFileNames(tempDir, "create-builder-scratch-"));
tempsAfter.removeAll(tempsBefore);
assertThat(tempsAfter).as("layer temp files must be deleted when layer creation fails").isEmpty();
}
@Test
void resolveDeletesIntermediateSourceTarTempFiles() throws Exception {
File tempDir = new File(System.getProperty("java.io.tmpdir"));