Skip to content

Commit 4e9add3

Browse files
authored
Make ArchiveUtils work on Windows (#683)
Signed-off-by: Thomas Vitale <ThomasVitale@users.noreply.github.com>
1 parent d2d26db commit 4e9add3

4 files changed

Lines changed: 115 additions & 28 deletions

File tree

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,10 @@ oci/
4040
!src/test/resources/oci/
4141
!src/test/resources/oci/**/*.json
4242
!src/test/resources/archives/*.zip
43+
44+
# macOS
45+
.DS_Store
46+
*.DS_Store
47+
**/.DS_Store
48+
.AppleDouble
49+
.LSOverride

src/main/java/land/oras/utils/ArchiveUtils.java

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -205,9 +205,14 @@ public static LocalPath tar(LocalPath sourceDir) {
205205
}
206206

207207
// Get posix permissions
208-
Set<PosixFilePermission> permissions = Files.getPosixFilePermissions(path);
209-
int mode = permissionsToMode(permissions);
210-
LOG.trace("Permissions: {}", permissions);
208+
int mode;
209+
if (OsUtils.isPosixFileSystemSupported()) {
210+
Set<PosixFilePermission> permissions = Files.getPosixFilePermissions(path);
211+
mode = permissionsToMode(permissions);
212+
LOG.trace("Permissions: {}", permissions);
213+
} else {
214+
mode = Files.isDirectory(path) || Files.isExecutable(path) ? 0755 : 0644;
215+
}
211216
LOG.trace("Mode: {}", mode);
212217

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

421426
// Restore file permissions (optional, based on your need)
422427
if (entry.isSymbolicLink()) {
423-
LOG.trace("Extracting symlink {} to: {}", outputPath, entry.getLinkName());
424-
Files.createSymbolicLink(outputPath, Paths.get(entry.getLinkName()));
428+
createSymbolicLink(outputPath, Paths.get(entry.getLinkName()));
425429
} else {
426430
try (OutputStream out = Files.newOutputStream(outputPath)) {
427431
tais.transferTo(out);
428432
}
429-
Files.setPosixFilePermissions(outputPath, convertToPosixPermissions(entry.getMode()));
433+
if (OsUtils.isPosixFileSystemSupported()) {
434+
Files.setPosixFilePermissions(outputPath, convertToPosixPermissions(entry.getMode()));
435+
}
430436
}
431437
}
432438
}
@@ -518,6 +524,24 @@ static LocalPath uncompressZstd(InputStream inputStream) {
518524
return LocalPath.of(tarFile, Const.DEFAULT_BLOB_MEDIA_TYPE);
519525
}
520526

527+
/**
528+
* Create a symbolic link, logging a warning if the operation is not supported.
529+
* Symlink creation may fail on systems that do not support symbolic links (e.g. Windows
530+
* without Developer Mode enabled or without administrator privileges).
531+
* @param link The link to create
532+
* @param target The target of the link
533+
*/
534+
private static void createSymbolicLink(Path link, Path target) {
535+
try {
536+
LOG.trace("Creating symlink {} -> {}", link, target);
537+
Files.createSymbolicLink(link, target);
538+
} catch (UnsupportedOperationException | SecurityException e) {
539+
LOG.warn("Cannot create symlink {}: {}", link.getFileName(), e.getMessage());
540+
} catch (IOException e) {
541+
throw new OrasException("Failed to create symlink: " + link.getFileName(), e);
542+
}
543+
}
544+
521545
/**
522546
* Opposite of convertToPosixPermissions. Convert PosixFilePermissions to mode
523547
* @param permissions The permissions
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*-
2+
* =LICENSE=
3+
* ORAS Java SDK
4+
* ===
5+
* Copyright (C) 2024 - 2026 ORAS
6+
* ===
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
* =LICENSEEND=
19+
*/
20+
21+
package land.oras.utils;
22+
23+
import java.nio.file.FileSystems;
24+
import org.jspecify.annotations.NullMarked;
25+
26+
/**
27+
* Operating system utilities
28+
*/
29+
@NullMarked
30+
public final class OsUtils {
31+
32+
/**
33+
* Hidden constructor
34+
*/
35+
private OsUtils() {}
36+
37+
/**
38+
* Whether the default file system supports POSIX file attributes.
39+
* @return true if POSIX file permissions are supported
40+
*/
41+
public static boolean isPosixFileSystemSupported() {
42+
return FileSystems.getDefault().supportedFileAttributeViews().contains("posix");
43+
}
44+
}

src/test/java/land/oras/utils/ArchiveUtilsTest.java

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -93,22 +93,24 @@ static void beforeAll() throws Exception {
9393
Files.writeString(file4, "file4");
9494

9595
// Create one symlink file3 -> file1
96-
Path file3 = dir1.resolve("file3");
97-
Files.createSymbolicLink(file3, file1);
98-
99-
// Add 777 permission to file2
100-
Files.setPosixFilePermissions(
101-
file2,
102-
Set.of(
103-
PosixFilePermission.OWNER_READ,
104-
PosixFilePermission.OWNER_WRITE,
105-
PosixFilePermission.OWNER_EXECUTE,
106-
PosixFilePermission.GROUP_READ,
107-
PosixFilePermission.GROUP_WRITE,
108-
PosixFilePermission.GROUP_EXECUTE,
109-
PosixFilePermission.OTHERS_READ,
110-
PosixFilePermission.OTHERS_WRITE,
111-
PosixFilePermission.OTHERS_EXECUTE));
96+
if (OsUtils.isPosixFileSystemSupported()) {
97+
Path file3 = dir1.resolve("file3");
98+
Files.createSymbolicLink(file3, file1);
99+
100+
// Add 777 permission to file2
101+
Files.setPosixFilePermissions(
102+
file2,
103+
Set.of(
104+
PosixFilePermission.OWNER_READ,
105+
PosixFilePermission.OWNER_WRITE,
106+
PosixFilePermission.OWNER_EXECUTE,
107+
PosixFilePermission.GROUP_READ,
108+
PosixFilePermission.GROUP_WRITE,
109+
PosixFilePermission.GROUP_EXECUTE,
110+
PosixFilePermission.OTHERS_READ,
111+
PosixFilePermission.OTHERS_WRITE,
112+
PosixFilePermission.OTHERS_EXECUTE));
113+
}
112114
}
113115

114116
@Test
@@ -175,7 +177,9 @@ void shouldCreateZipAndExtractIt() throws Exception {
175177
assertTrue(Files.exists(extractedDir.resolve("dir2")), "dir2 should exist");
176178
assertTrue(Files.exists(extractedDir.resolve("dir1").resolve("file1")), "file1 should exist");
177179
assertTrue(Files.exists(extractedDir.resolve("dir2").resolve("file2")), "file2 should exist");
178-
assertTrue(Files.exists(extractedDir.resolve("dir1").resolve("file3")), "file3 should exist");
180+
if (OsUtils.isPosixFileSystemSupported()) {
181+
assertTrue(Files.exists(extractedDir.resolve("dir1").resolve("file3")), "file3 should exist");
182+
}
179183
assertTrue(Files.exists(extractedDir.resolve("dir2").resolve("dir3")), "dir3 should exist");
180184
assertTrue(Files.exists(extractedDir.resolve("dir2").resolve("dir3").resolve("file4")), "file4 should exist");
181185

@@ -218,7 +222,9 @@ void shouldCreateTarGzAndExtractIt() throws Exception {
218222
assertTrue(Files.exists(extractedDir.resolve("dir2")), "dir2 should exist");
219223
assertTrue(Files.exists(extractedDir.resolve("dir1").resolve("file1")), "file1 should exist");
220224
assertTrue(Files.exists(extractedDir.resolve("dir2").resolve("file2")), "file2 should exist");
221-
assertTrue(Files.exists(extractedDir.resolve("dir1").resolve("file3")), "file3 should exist");
225+
if (OsUtils.isPosixFileSystemSupported()) {
226+
assertTrue(Files.exists(extractedDir.resolve("dir1").resolve("file3")), "file3 should exist");
227+
}
222228
assertTrue(Files.exists(extractedDir.resolve("dir2").resolve("dir3")), "dir3 should exist");
223229
assertTrue(Files.exists(extractedDir.resolve("dir2").resolve("dir3").resolve("file4")), "file4 should exist");
224230

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

240246
// Ensure symlink is extracted
241-
assertTrue(Files.isSymbolicLink(extractedDir.resolve("dir1").resolve("file3")), "file3 should be symlink");
247+
if (OsUtils.isPosixFileSystemSupported()) {
248+
assertTrue(Files.isSymbolicLink(extractedDir.resolve("dir1").resolve("file3")), "file3 should be symlink");
249+
}
242250

243251
// To temporary
244252
Path temp = ArchiveUtils.uncompressuntar(compressedArchive, directory.getMediaType());
@@ -290,7 +298,9 @@ void shouldCreateTarZstdAndExtractIt() throws Exception {
290298
assertTrue(Files.exists(extractedDir.resolve("dir2")), "dir2 should exist");
291299
assertTrue(Files.exists(extractedDir.resolve("dir1").resolve("file1")), "file1 should exist");
292300
assertTrue(Files.exists(extractedDir.resolve("dir2").resolve("file2")), "file2 should exist");
293-
assertTrue(Files.exists(extractedDir.resolve("dir1").resolve("file3")), "file3 should exist");
301+
if (OsUtils.isPosixFileSystemSupported()) {
302+
assertTrue(Files.exists(extractedDir.resolve("dir1").resolve("file3")), "file3 should exist");
303+
}
294304
assertTrue(Files.exists(extractedDir.resolve("dir2").resolve("dir3")), "dir3 should exist");
295305
assertTrue(Files.exists(extractedDir.resolve("dir2").resolve("dir3").resolve("file4")), "file4 should exist");
296306

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

312322
// Ensure symlink is extracted
313-
assertTrue(Files.isSymbolicLink(extractedDir.resolve("dir1").resolve("file3")), "file3 should be symlink");
323+
if (OsUtils.isPosixFileSystemSupported()) {
324+
assertTrue(Files.isSymbolicLink(extractedDir.resolve("dir1").resolve("file3")), "file3 should be symlink");
325+
}
314326

315327
// To temporary
316328
Path temp = ArchiveUtils.uncompressuntar(compressedArchive, directory.getMediaType());

0 commit comments

Comments
 (0)