100100import java .net .URLDecoder ;
101101import java .nio .file .Files ;
102102import java .nio .file .Path ;
103+ import java .nio .file .StandardCopyOption ;
103104import java .text .DateFormat ;
104105import java .text .SimpleDateFormat ;
105106import java .util .ArrayList ;
@@ -176,9 +177,10 @@ protected SkylineTool getToolFromZip(MultipartFile zip) throws IOException
176177 {
177178 ZipEntry zipEntry ;
178179 while ((zipEntry = zipStream .getNextEntry ()) != null &&
179- (tool == null || tool . getIcon () == null ))
180+ (tool == null || toolIcon == null ))
180181 {
181- if (zipEntry .getName ().toLowerCase ().startsWith ("tool-inf/" ))
182+ String entryLower = zipEntry .getName ().toLowerCase ();
183+ if (entryLower .startsWith ("tool-inf/" ) && !entryLower .startsWith ("tool-inf/docs/" ))
182184 {
183185 String lowerBaseName = new File (zipEntry .getName ()).getName ().toLowerCase ();
184186
@@ -229,14 +231,46 @@ protected byte[] unzip(ZipInputStream stream)
229231 }
230232 }
231233
234+ protected static boolean extractDocsFromZip (Path zipFile , Path containerDir ) throws IOException
235+ {
236+ Path docsDir = containerDir .resolve ("docs" );
237+ boolean extracted = false ;
238+ try (ZipFile zf = new ZipFile (zipFile .toFile ()))
239+ {
240+ Enumeration <? extends ZipEntry > entries = zf .entries ();
241+ while (entries .hasMoreElements ())
242+ {
243+ ZipEntry entry = entries .nextElement ();
244+ String name = entry .getName ();
245+ if (!name .toLowerCase ().startsWith ("tool-inf/docs/" ) || entry .isDirectory ())
246+ continue ;
247+ // Strip "tool-inf/docs/" prefix to get relative path within docs dir
248+ String relativePath = name .substring ("tool-inf/docs/" .length ());
249+ if (relativePath .isEmpty ())
250+ continue ;
251+ Path destPath = docsDir .resolve (relativePath ).normalize ();
252+ // Zip-slip protection
253+ if (!destPath .startsWith (docsDir .normalize ()))
254+ throw new IOException ("Zip entry outside target directory: " + name );
255+ Files .createDirectories (destPath .getParent ());
256+ try (InputStream in = zf .getInputStream (entry ))
257+ {
258+ Files .copy (in , destPath , StandardCopyOption .REPLACE_EXISTING );
259+ }
260+ extracted = true ;
261+ }
262+ }
263+ return extracted ;
264+ }
265+
232266 public static File makeFile (Container c , String filename )
233267 {
234- return new File ( getLocalPath (c ), FileUtil .makeLegalName (filename ));
268+ return getLocalPath (c ). resolve ( FileUtil .makeLegalName (filename )). toFile ( );
235269 }
236270
237- public static File getLocalPath (Container c )
271+ public static Path getLocalPath (Container c )
238272 {
239- return FileContentService .get ().getFileRootPath (c , FileContentService .ContentType .files ). toFile () ;
273+ return FileContentService .get ().getFileRootPath (c , FileContentService .ContentType .files );
240274 }
241275
242276 protected Container makeContainer (Container parent , String folderName , List <User > users , Role role ) throws IOException
@@ -383,7 +417,7 @@ public static ArrayList<String> getToolRelevantUsers(SkylineTool tool, Role[] ro
383417 return new ArrayList <>(users );
384418 }
385419
386- public static HashMap <String , String > getSupplementaryFiles (SkylineTool tool )
420+ public static HashMap <String , String > getSupplementaryFiles (SkylineTool tool ) throws IOException
387421 {
388422 // Store supporting files in map <url, icon url>
389423 final String [] knownExtensions = {"pdf" , "zip" };
@@ -401,15 +435,15 @@ public static HashMap<String, String> getSupplementaryFiles(SkylineTool tool)
401435 return suppFiles ;
402436 }
403437
404- public static HashSet <String > getSupplementaryFileBasenames (SkylineTool tool )
438+ public static HashSet <String > getSupplementaryFileBasenames (SkylineTool tool ) throws IOException
405439 {
406440 HashSet <String > suppFiles = new HashSet <>();
407- File localToolDir = getLocalPath (tool .lookupContainer ());
408- for ( String suppFile : localToolDir .list ())
441+ Path localToolDir = getLocalPath (tool .lookupContainer ());
442+ try ( var stream = Files .list (localToolDir ))
409443 {
410- final String basename = new File ( suppFile ). getName ();
411- if (! basename .startsWith ("." ) && !basename .equals (tool .getZipName ()) && !basename .equals ("icon.png" ))
412- suppFiles . add ( suppFile );
444+ stream . map ( p -> p . getFileName (). toString ())
445+ . filter ( name -> ! name .startsWith ("." ) && !name .equals (tool .getZipName ()) && !name .equals ("icon.png" ) && ! name . equals ( "docs " ))
446+ . forEach ( suppFiles :: add );
413447 }
414448 return suppFiles ;
415449 }
@@ -583,9 +617,19 @@ else if (!getContainer().hasPermission(getUser(), InsertPermission.class))
583617 {
584618 Container c = makeContainer (getContainer (), folderName , toolOwnersUsers , RoleManager .getRole (EditorRole .class ));
585619 copyContainerPermissions (existingVersionContainer , c );
586- zip .transferTo (makeFile (c , zip .getOriginalFilename ()));
620+ File storedZip = makeFile (c , zip .getOriginalFilename ());
621+ zip .transferTo (storedZip );
587622 tool .writeIconToFile (makeFile (c , "icon.png" ), "png" );
588623
624+ // Extract docs from tool-inf/docs/ in the ZIP; carry forward from previous version if absent
625+ boolean hasDocs = extractDocsFromZip (storedZip .toPath (), getLocalPath (c ));
626+ if (!hasDocs && existingVersionContainer != null )
627+ {
628+ Path oldDocs = getLocalPath (existingVersionContainer ).resolve ("docs" );
629+ if (Files .isDirectory (oldDocs ))
630+ FileUtil .copyDirectory (oldDocs , getLocalPath (c ).resolve ("docs" ));
631+ }
632+
589633 if (copyFiles != null && existingVersionContainer != null )
590634 for (String copyFile : copyFiles )
591635 FileUtils .copyFile (makeFile (existingVersionContainer , copyFile ), makeFile (c , copyFile ), true );
0 commit comments