Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,10 @@ oci/
!src/test/resources/oci/
!src/test/resources/oci/**/*.json
!src/test/resources/archives/*.zip

# macOS
.DS_Store
*.DS_Store
**/.DS_Store
.AppleDouble
.LSOverride
38 changes: 31 additions & 7 deletions src/main/java/land/oras/utils/ArchiveUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -205,9 +205,14 @@ public static LocalPath tar(LocalPath sourceDir) {
}

// Get posix permissions
Set<PosixFilePermission> permissions = Files.getPosixFilePermissions(path);
int mode = permissionsToMode(permissions);
LOG.trace("Permissions: {}", permissions);
int mode;
if (OsUtils.isPosixFileSystemSupported()) {
Set<PosixFilePermission> permissions = Files.getPosixFilePermissions(path);
mode = permissionsToMode(permissions);
LOG.trace("Permissions: {}", permissions);
} else {
mode = Files.isDirectory(path) || Files.isExecutable(path) ? 0755 : 0644;
}
LOG.trace("Mode: {}", mode);

// Set UID, GID, Uname, Gname to zero or empty
Expand Down Expand Up @@ -371,7 +376,7 @@ static void unzip(InputStream fis, Path target) {
String linkStr = asiField != null
? asiField.getLinkedFile()
: new String(zais.readAllBytes(), StandardCharsets.UTF_8);
Files.createSymbolicLink(outputPath, Paths.get(linkStr));
createSymbolicLink(outputPath, Paths.get(linkStr));
} else {
LOG.debug("Extracting file: {}", entry.getName());
Files.createDirectories(outputPath.getParent());
Expand Down Expand Up @@ -420,13 +425,14 @@ public static void untar(InputStream fis, Path target) {

// Restore file permissions (optional, based on your need)
if (entry.isSymbolicLink()) {
LOG.trace("Extracting symlink {} to: {}", outputPath, entry.getLinkName());
Files.createSymbolicLink(outputPath, Paths.get(entry.getLinkName()));
createSymbolicLink(outputPath, Paths.get(entry.getLinkName()));
} else {
try (OutputStream out = Files.newOutputStream(outputPath)) {
tais.transferTo(out);
}
Files.setPosixFilePermissions(outputPath, convertToPosixPermissions(entry.getMode()));
if (OsUtils.isPosixFileSystemSupported()) {
Files.setPosixFilePermissions(outputPath, convertToPosixPermissions(entry.getMode()));
}
}
}
}
Expand Down Expand Up @@ -518,6 +524,24 @@ static LocalPath uncompressZstd(InputStream inputStream) {
return LocalPath.of(tarFile, Const.DEFAULT_BLOB_MEDIA_TYPE);
}

/**
* Create a symbolic link, logging a warning if the operation is not supported.
* Symlink creation may fail on systems that do not support symbolic links (e.g. Windows
* without Developer Mode enabled or without administrator privileges).
* @param link The link to create
* @param target The target of the link
*/
private static void createSymbolicLink(Path link, Path target) {
try {
LOG.trace("Creating symlink {} -> {}", link, target);
Files.createSymbolicLink(link, target);
} catch (UnsupportedOperationException | SecurityException e) {
LOG.warn("Cannot create symlink {}: {}", link.getFileName(), e.getMessage());
} catch (IOException e) {
throw new OrasException("Failed to create symlink: " + link.getFileName(), e);
}
}

/**
* Opposite of convertToPosixPermissions. Convert PosixFilePermissions to mode
* @param permissions The permissions
Expand Down
44 changes: 44 additions & 0 deletions src/main/java/land/oras/utils/OsUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*-
* =LICENSE=
* ORAS Java SDK
* ===
* Copyright (C) 2024 - 2026 ORAS
* ===
* 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.
* =LICENSEEND=
*/

package land.oras.utils;

import java.nio.file.FileSystems;
import org.jspecify.annotations.NullMarked;

/**
* Operating system utilities
*/
@NullMarked
public final class OsUtils {

/**
* Hidden constructor
*/
private OsUtils() {}

/**
* Whether the default file system supports POSIX file attributes.
* @return true if POSIX file permissions are supported
*/
public static boolean isPosixFileSystemSupported() {
return FileSystems.getDefault().supportedFileAttributeViews().contains("posix");
}
}
54 changes: 33 additions & 21 deletions src/test/java/land/oras/utils/ArchiveUtilsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -93,22 +93,24 @@ static void beforeAll() throws Exception {
Files.writeString(file4, "file4");

// Create one symlink file3 -> file1
Path file3 = dir1.resolve("file3");
Files.createSymbolicLink(file3, file1);

// Add 777 permission to file2
Files.setPosixFilePermissions(
file2,
Set.of(
PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE,
PosixFilePermission.OWNER_EXECUTE,
PosixFilePermission.GROUP_READ,
PosixFilePermission.GROUP_WRITE,
PosixFilePermission.GROUP_EXECUTE,
PosixFilePermission.OTHERS_READ,
PosixFilePermission.OTHERS_WRITE,
PosixFilePermission.OTHERS_EXECUTE));
if (OsUtils.isPosixFileSystemSupported()) {
Path file3 = dir1.resolve("file3");
Files.createSymbolicLink(file3, file1);

// Add 777 permission to file2
Files.setPosixFilePermissions(
file2,
Set.of(
PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE,
PosixFilePermission.OWNER_EXECUTE,
PosixFilePermission.GROUP_READ,
PosixFilePermission.GROUP_WRITE,
PosixFilePermission.GROUP_EXECUTE,
PosixFilePermission.OTHERS_READ,
PosixFilePermission.OTHERS_WRITE,
PosixFilePermission.OTHERS_EXECUTE));
}
}

@Test
Expand Down Expand Up @@ -175,7 +177,9 @@ void shouldCreateZipAndExtractIt() throws Exception {
assertTrue(Files.exists(extractedDir.resolve("dir2")), "dir2 should exist");
assertTrue(Files.exists(extractedDir.resolve("dir1").resolve("file1")), "file1 should exist");
assertTrue(Files.exists(extractedDir.resolve("dir2").resolve("file2")), "file2 should exist");
assertTrue(Files.exists(extractedDir.resolve("dir1").resolve("file3")), "file3 should exist");
if (OsUtils.isPosixFileSystemSupported()) {
assertTrue(Files.exists(extractedDir.resolve("dir1").resolve("file3")), "file3 should exist");
}
assertTrue(Files.exists(extractedDir.resolve("dir2").resolve("dir3")), "dir3 should exist");
assertTrue(Files.exists(extractedDir.resolve("dir2").resolve("dir3").resolve("file4")), "file4 should exist");

Expand Down Expand Up @@ -218,7 +222,9 @@ void shouldCreateTarGzAndExtractIt() throws Exception {
assertTrue(Files.exists(extractedDir.resolve("dir2")), "dir2 should exist");
assertTrue(Files.exists(extractedDir.resolve("dir1").resolve("file1")), "file1 should exist");
assertTrue(Files.exists(extractedDir.resolve("dir2").resolve("file2")), "file2 should exist");
assertTrue(Files.exists(extractedDir.resolve("dir1").resolve("file3")), "file3 should exist");
if (OsUtils.isPosixFileSystemSupported()) {
assertTrue(Files.exists(extractedDir.resolve("dir1").resolve("file3")), "file3 should exist");
}
assertTrue(Files.exists(extractedDir.resolve("dir2").resolve("dir3")), "dir3 should exist");
assertTrue(Files.exists(extractedDir.resolve("dir2").resolve("dir3").resolve("file4")), "file4 should exist");

Expand All @@ -238,7 +244,9 @@ void shouldCreateTarGzAndExtractIt() throws Exception {
"file4 content should match");

// Ensure symlink is extracted
assertTrue(Files.isSymbolicLink(extractedDir.resolve("dir1").resolve("file3")), "file3 should be symlink");
if (OsUtils.isPosixFileSystemSupported()) {
assertTrue(Files.isSymbolicLink(extractedDir.resolve("dir1").resolve("file3")), "file3 should be symlink");
}

// To temporary
Path temp = ArchiveUtils.uncompressuntar(compressedArchive, directory.getMediaType());
Expand Down Expand Up @@ -290,7 +298,9 @@ void shouldCreateTarZstdAndExtractIt() throws Exception {
assertTrue(Files.exists(extractedDir.resolve("dir2")), "dir2 should exist");
assertTrue(Files.exists(extractedDir.resolve("dir1").resolve("file1")), "file1 should exist");
assertTrue(Files.exists(extractedDir.resolve("dir2").resolve("file2")), "file2 should exist");
assertTrue(Files.exists(extractedDir.resolve("dir1").resolve("file3")), "file3 should exist");
if (OsUtils.isPosixFileSystemSupported()) {
assertTrue(Files.exists(extractedDir.resolve("dir1").resolve("file3")), "file3 should exist");
}
assertTrue(Files.exists(extractedDir.resolve("dir2").resolve("dir3")), "dir3 should exist");
assertTrue(Files.exists(extractedDir.resolve("dir2").resolve("dir3").resolve("file4")), "file4 should exist");

Expand All @@ -310,7 +320,9 @@ void shouldCreateTarZstdAndExtractIt() throws Exception {
"file4 content should match");

// Ensure symlink is extracted
assertTrue(Files.isSymbolicLink(extractedDir.resolve("dir1").resolve("file3")), "file3 should be symlink");
if (OsUtils.isPosixFileSystemSupported()) {
assertTrue(Files.isSymbolicLink(extractedDir.resolve("dir1").resolve("file3")), "file3 should be symlink");
}

// To temporary
Path temp = ArchiveUtils.uncompressuntar(compressedArchive, directory.getMediaType());
Expand Down
Loading