diff --git a/pom.xml b/pom.xml index 78d5a945..4cab6349 100644 --- a/pom.xml +++ b/pom.xml @@ -59,6 +59,7 @@ 3.0.4 1.28.0 1.5.7-7 + 1.11 1.83 @@ -170,6 +171,11 @@ testcontainers-junit-jupiter ${testcontainer.version} + + org.tukaani + xz + ${xz.version} + org.wiremock wiremock-standalone @@ -209,6 +215,10 @@ org.slf4j slf4j-api + + org.tukaani + xz + tools.jackson.core jackson-core diff --git a/src/main/java/land/oras/utils/ArchiveUtils.java b/src/main/java/land/oras/utils/ArchiveUtils.java index 55b8adf7..0d02f047 100644 --- a/src/main/java/land/oras/utils/ArchiveUtils.java +++ b/src/main/java/land/oras/utils/ArchiveUtils.java @@ -40,6 +40,7 @@ import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream; +import org.apache.commons.compress.compressors.xz.XZCompressorOutputStream; import org.apache.commons.compress.compressors.zstandard.ZstdCompressorInputStream; import org.apache.commons.compress.compressors.zstandard.ZstdCompressorOutputStream; import org.jspecify.annotations.NullMarked; @@ -322,6 +323,22 @@ static LocalPath compressZstd(LocalPath tarFile) { return LocalPath.of(tarGzFile, Const.BLOB_DIR_ZSTD_MEDIA_TYPE); } + static LocalPath compressXz(LocalPath tarFile) { + LOG.trace("Compressing tar file to xz archive"); + Path tarXzFile = Paths.get(tarFile.toString() + ".xz"); + try (InputStream fis = Files.newInputStream(tarFile.getPath()); + BufferedInputStream bis = new BufferedInputStream(fis); + OutputStream fos = Files.newOutputStream(tarXzFile); + BufferedOutputStream bos = new BufferedOutputStream(fos); + XZCompressorOutputStream xzos = new XZCompressorOutputStream(bos)) { + + bis.transferTo(xzos); + } catch (IOException e) { + throw new OrasException("Failed to compress tar file to xz archive", e); + } + return LocalPath.of(tarXzFile, Const.BLOB_DIR_XZ_MEDIA_TYPE); + } + static LocalPath compressGzip(LocalPath tarFile) { LOG.trace("Compressing tar file to gz archive"); Path tarGzFile = Paths.get(tarFile.toString() + ".gz"); @@ -353,6 +370,20 @@ static LocalPath uncompressGzip(InputStream inputStream) { return LocalPath.of(tarFile, Const.DEFAULT_BLOB_MEDIA_TYPE); } + static LocalPath uncompressXz(InputStream inputStream) { + LOG.trace("Uncompressing tar.xz file"); + Path tarFile = createTempTar(); + try (BufferedInputStream bis = new BufferedInputStream(inputStream); + XZCompressorOutputStream xzos = + new XZCompressorOutputStream(new BufferedOutputStream(Files.newOutputStream(tarFile)))) { + + bis.transferTo(xzos); + } catch (IOException e) { + throw new OrasException("Failed to uncompress tar.xz file", e); + } + return LocalPath.of(tarFile, Const.DEFAULT_BLOB_MEDIA_TYPE); + } + static LocalPath uncompressZstd(InputStream inputStream) { LOG.trace("Uncompressing zstd file"); Path tarFile = createTempTar(); diff --git a/src/main/java/land/oras/utils/Const.java b/src/main/java/land/oras/utils/Const.java index 9b580f7a..525e894f 100644 --- a/src/main/java/land/oras/utils/Const.java +++ b/src/main/java/land/oras/utils/Const.java @@ -133,6 +133,11 @@ private Const() { */ public static final String BLOB_DIR_ZSTD_MEDIA_TYPE = "application/vnd.oci.image.layer.v1.tar+zstd"; + /** + * The blob directory media type for xz compression + */ + public static final String BLOB_DIR_XZ_MEDIA_TYPE = "application/vnd.oci.image.layer.v1.tar+xz"; + /** * The default artifact media type if not specified */ diff --git a/src/main/java/land/oras/utils/SupportedCompression.java b/src/main/java/land/oras/utils/SupportedCompression.java index a6e783f7..3c8e0fc6 100644 --- a/src/main/java/land/oras/utils/SupportedCompression.java +++ b/src/main/java/land/oras/utils/SupportedCompression.java @@ -59,7 +59,12 @@ public enum SupportedCompression { /** * ZSTD */ - ZSTD(Const.BLOB_DIR_ZSTD_MEDIA_TYPE, ArchiveUtils::compressZstd, ArchiveUtils::uncompressZstd); + ZSTD(Const.BLOB_DIR_ZSTD_MEDIA_TYPE, ArchiveUtils::compressZstd, ArchiveUtils::uncompressZstd), + + /** + * XZ + */ + XZ(Const.BLOB_DIR_XZ_MEDIA_TYPE, ArchiveUtils::compressXz, ArchiveUtils::uncompressXz); /** * The media type diff --git a/src/test/java/land/oras/utils/ArchiveUtilsTest.java b/src/test/java/land/oras/utils/ArchiveUtilsTest.java index a3c1c993..8ead0408 100644 --- a/src/test/java/land/oras/utils/ArchiveUtilsTest.java +++ b/src/test/java/land/oras/utils/ArchiveUtilsTest.java @@ -64,6 +64,9 @@ class ArchiveUtilsTest { @TempDir(cleanup = CleanupMode.ON_SUCCESS) private static Path targetGzDir; + @TempDir(cleanup = CleanupMode.ON_SUCCESS) + private static Path targetXzDir; + @TempDir(cleanup = CleanupMode.ON_SUCCESS) private static Path targetZstdDir; @@ -208,6 +211,26 @@ void shouldExtractSeveralExistingArchive(String file) { ArchiveUtils.uncompressuntar(archive, existingArchiveDir, SupportedCompression.GZIP.getMediaType()); } + @Test + void shouldCreateTarXzAndExtractIt() throws Exception { + LocalPath directory = LocalPath.of(archiveDir, Const.BLOB_DIR_XZ_MEDIA_TYPE); + LocalPath archive = ArchiveUtils.tar(directory); + LOG.info("Archive created: {}", archive); + Path compressedArchive = + ArchiveUtils.compress(archive, directory.getMediaType()).getPath(); + + assertTrue(Files.exists(compressedArchive), "Archive should exist"); + + Path uncompressedArchive = ArchiveUtils.uncompress( + Files.newInputStream(compressedArchive), Const.BLOB_DIR_XZ_MEDIA_TYPE) + .getPath(); + ArchiveUtils.untar(Files.newInputStream(uncompressedArchive), targetXzDir); + + // To temporary + Path temp = ArchiveUtils.uncompressuntar(compressedArchive, directory.getMediaType()); + assertTrue(Files.exists(temp), "Temp should exist"); + } + @Test void shouldCreateTarZstdAndExtractIt() throws Exception { LocalPath directory = LocalPath.of(archiveDir, Const.BLOB_DIR_ZSTD_MEDIA_TYPE);