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);