2525import java .io .IOException ;
2626import java .io .InputStream ;
2727import java .io .OutputStream ;
28+ import java .nio .charset .StandardCharsets ;
2829import java .nio .file .Files ;
2930import java .nio .file .Path ;
3031import java .nio .file .Paths ;
3536import java .util .stream .Stream ;
3637import land .oras .LocalPath ;
3738import land .oras .exception .OrasException ;
39+ import org .apache .commons .compress .archivers .ArchiveEntry ;
3840import org .apache .commons .compress .archivers .tar .TarArchiveEntry ;
3941import org .apache .commons .compress .archivers .tar .TarArchiveInputStream ;
4042import org .apache .commons .compress .archivers .tar .TarArchiveOutputStream ;
43+ import org .apache .commons .compress .archivers .zip .AsiExtraField ;
44+ import org .apache .commons .compress .archivers .zip .ZipArchiveEntry ;
45+ import org .apache .commons .compress .archivers .zip .ZipArchiveInputStream ;
46+ import org .apache .commons .compress .archivers .zip .ZipArchiveOutputStream ;
4147import org .apache .commons .compress .compressors .gzip .GzipCompressorInputStream ;
4248import org .apache .commons .compress .compressors .gzip .GzipCompressorOutputStream ;
4349import org .apache .commons .compress .compressors .zstandard .ZstdCompressorInputStream ;
@@ -74,6 +80,18 @@ public static Path createTempTar() {
7480 }
7581 }
7682
83+ /**
84+ * Create a temporary zip file when uploading directory layers with zip media type
85+ * @return The path to the zip file
86+ */
87+ public static Path createTempZip () {
88+ try {
89+ return Files .createTempFile ("oras" , ".zip" );
90+ } catch (IOException e ) {
91+ throw new OrasException ("Failed to create temporary zip file" , e );
92+ }
93+ }
94+
7795 /**
7896 * Create a temporary directory
7997 * @return The path to the temporary directory
@@ -86,6 +104,64 @@ public static Path createTempDir() {
86104 }
87105 }
88106
107+ /**
108+ * Zip a local source dire and return a temporary zip file as a local path
109+ * @param sourceDir The source directory
110+ * @return The local path to the zip file
111+ */
112+ public static LocalPath zip (LocalPath sourceDir ) {
113+ Path zipFile = createTempZip ();
114+ boolean isAbsolute = sourceDir .getPath ().isAbsolute ();
115+ try (OutputStream fos = Files .newOutputStream (zipFile );
116+ BufferedOutputStream bos = new BufferedOutputStream (fos );
117+ ZipArchiveOutputStream zaos = new ZipArchiveOutputStream (bos )) {
118+ try (Stream <Path > paths = Files .walk (sourceDir .getPath ())) {
119+ paths .forEach (path -> {
120+ LOG .trace ("Visiting path: {}" , path );
121+ try {
122+ Path baseName = isAbsolute ? sourceDir .getPath ().getFileName () : sourceDir .getPath ();
123+ Path relativePath = baseName .resolve (sourceDir .getPath ().relativize (path ));
124+ if (relativePath .toString ().isEmpty ()) {
125+ LOG .trace ("Skipping root directory: {}" , path );
126+ return ;
127+ }
128+ String entryName = relativePath .toString ();
129+ if (Files .isSymbolicLink (path )) {
130+ LOG .trace ("Adding symlink entry to zip: {}" , entryName );
131+ Path linkTarget = Files .readSymbolicLink (path );
132+ ZipArchiveEntry entry = new ZipArchiveEntry (entryName );
133+ AsiExtraField asiField = new AsiExtraField ();
134+ asiField .setLinkedFile (linkTarget .toString ());
135+ // 0120000 = S_IFLNK (symlink file type), 0755 = permissions
136+ asiField .setMode (0120755 );
137+ entry .addExtraField (asiField );
138+ entry .setSize (0 );
139+ zaos .putArchiveEntry (entry );
140+ } else if (Files .isDirectory (path )) {
141+ LOG .trace ("Adding directory entry to zip: {}" , entryName + "/" );
142+ ZipArchiveEntry entry = new ZipArchiveEntry (entryName + "/" );
143+ zaos .putArchiveEntry (entry );
144+ } else {
145+ LOG .trace ("Adding file entry to zip: {}" , entryName );
146+ ZipArchiveEntry entry = new ZipArchiveEntry (entryName );
147+ entry .setSize (Files .size (path ));
148+ zaos .putArchiveEntry (entry );
149+ try (InputStream fis = Files .newInputStream (path )) {
150+ fis .transferTo (zaos );
151+ }
152+ }
153+ zaos .closeArchiveEntry ();
154+ } catch (IOException e ) {
155+ throw new OrasException ("Failed to create zip file" , e );
156+ }
157+ });
158+ }
159+ } catch (IOException e ) {
160+ throw new OrasException ("Failed to create zip file" , e );
161+ }
162+ return LocalPath .of (zipFile , Const .ZIP_MEDIA_TYPE );
163+ }
164+
89165 /**
90166 * Create a tar.gz file from a directory
91167 * @param sourceDir The source directory
@@ -177,7 +253,7 @@ public static LocalPath tarcompress(LocalPath sourceDir, String mediaType) {
177253 * @param target The target directory
178254 * @throws IOException
179255 */
180- static void ensureSafeEntry (TarArchiveEntry entry , Path target ) throws IOException {
256+ static void ensureSafeEntry (ArchiveEntry entry , Path target ) throws IOException {
181257 // Prevent path traversal attacks
182258 Path outputPath = target .resolve (entry .getName ()).normalize ();
183259 Path normalizedTarget = target .toAbsolutePath ().normalize ();
@@ -237,6 +313,80 @@ public static Path untar(Path path) {
237313 return tempDir ;
238314 }
239315
316+ /**
317+ * Extract a zip file to a target directory
318+ * @param path The zip file
319+ * @param target The target directory
320+ */
321+ public static void unzip (Path path , Path target ) {
322+ try {
323+ unzip (Files .newInputStream (path ), target );
324+ } catch (IOException e ) {
325+ throw new OrasException ("Failed to extract zip file" , e );
326+ }
327+ }
328+
329+ /**
330+ * Unzip a file to a temporary directory and return the local path to the temporary directory
331+ * @param fis The zip file input stream
332+ * @return The local path to the temporary directory
333+ */
334+ static LocalPath unzip (InputStream fis ) {
335+ Path tempDir = createTempDir ();
336+ unzip (fis , tempDir );
337+ return LocalPath .of (tempDir );
338+ }
339+
340+ /**
341+ * Extract a zip file to a target directory
342+ * @param fis The zip file input stream
343+ * @param target The target directory
344+ */
345+ static void unzip (InputStream fis , Path target ) {
346+ // Open the zip file for reading
347+ try {
348+ try (BufferedInputStream bis = new BufferedInputStream (fis );
349+ ZipArchiveInputStream zais = new ZipArchiveInputStream (bis )) {
350+ ZipArchiveEntry entry ;
351+
352+ // Iterate through zip entries
353+ while ((entry = zais .getNextEntry ()) != null ) {
354+
355+ // Prevent path traversal attacks
356+ Path outputPath = target .resolve (entry .getName ()).normalize ();
357+
358+ // Check if the entry is outside the target directory
359+ ensureSafeEntry (entry , target );
360+
361+ if (entry .isDirectory ()) {
362+ LOG .debug ("Extracting directory: {}" , entry .getName ());
363+ Files .createDirectories (outputPath );
364+ }
365+ // Check symlink from AsiExtraField
366+ else {
367+ AsiExtraField asiField = (AsiExtraField ) entry .getExtraField (new AsiExtraField ().getHeaderId ());
368+ if (entry .isUnixSymlink () || (asiField != null && asiField .isLink ())) {
369+ LOG .debug ("Extracting symlink: {}" , entry .getName ());
370+ Files .createDirectories (outputPath .getParent ());
371+ String linkStr = asiField != null
372+ ? asiField .getLinkedFile ()
373+ : new String (zais .readAllBytes (), StandardCharsets .UTF_8 );
374+ Files .createSymbolicLink (outputPath , Paths .get (linkStr ));
375+ } else {
376+ LOG .debug ("Extracting file: {}" , entry .getName ());
377+ Files .createDirectories (outputPath .getParent ());
378+ try (OutputStream out = Files .newOutputStream (outputPath )) {
379+ zais .transferTo (out );
380+ }
381+ }
382+ }
383+ }
384+ }
385+ } catch (IOException e ) {
386+ throw new OrasException ("Failed to extract zip file" , e );
387+ }
388+ }
389+
240390 /**
241391 * Extract a tar file to a target directory
242392 * @param fis The archive stream
@@ -308,7 +458,7 @@ public static LocalPath uncompress(InputStream is, String mediaType) {
308458
309459 static LocalPath compressZstd (LocalPath tarFile ) {
310460 LOG .trace ("Compressing tar file to zstd archive" );
311- Path tarGzFile = Paths .get (tarFile . toString () + ".gz" );
461+ Path tarGzFile = Paths .get (tarFile + ".gz" );
312462 try (InputStream fis = Files .newInputStream (tarFile .getPath ());
313463 BufferedInputStream bis = new BufferedInputStream (fis );
314464 OutputStream fos = Files .newOutputStream (tarGzFile );
0 commit comments