CVE-2023-3432: Server-Side Request Forgery (SSRF) (CWE-918)
value: Server-Side Request Forgery (SSRF) in GitHub repository plantuml/plantuml prior to 1.2023.9.
CWE description: The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
CVSS v3 base score: 10
Published: 2023-06-27T15:15Z
Repository: https://github.com/plantuml/plantuml
Language: Java
Files changed: gradle.properties, src/net/sourceforge/plantuml/file/AParentFolderRegular.java, src/net/sourceforge/plantuml/filesdiagram/FilesEntry.java, src/net/sourceforge/plantuml/filesdiagram/FilesListing.java, src/net/sourceforge/plantuml/filesdiagram/FilesType.java, src/net/sourceforge/plantuml/klimt/sprite/SpriteColor.java, src/net/sourceforge/plantuml/nwdiag/NwDiagram.java, src/net/sourceforge/plantuml/nwdiag/core/NServer.java, src/net/sourceforge/plantuml/security/SURL.java, src/net/sourceforge/plantuml/tim/TFunctionImpl.java, src/net/sourceforge/plantuml/version/LicenseInfo.java, src/net/sourceforge/plantuml/version/Version.java
Commit date: 2023-06-12 22:49:47 +0200
Commit message: fix: improve filelist support and nwdiag

Diff: diff --git a/gradle.properties b/gradle.properties
index 0d4293c8e..fe3b6bc48 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,4 @@
 # Warning, "version" should be the same in gradle.properties and Version.java
 # Any idea anyone how to magically synchronize those :-) ?
-version = 1.2023.9beta3
+version = 1.2023.9beta4
 org.gradle.workers.max = 3
\ No newline at end of file
diff --git a/src/net/sourceforge/plantuml/file/AParentFolderRegular.java b/src/net/sourceforge/plantuml/file/AParentFolderRegular.java
index 439c25342..0a7161c12 100644
--- a/src/net/sourceforge/plantuml/file/AParentFolderRegular.java
+++ b/src/net/sourceforge/plantuml/file/AParentFolderRegular.java
@@ -55,18 +55,18 @@ public class AParentFolderRegular implements AParentFolder {
 
 	public AFile getAFile(String nameOrPath) throws IOException {
 		final SFile filecurrent;
 		// Log.info("AParentFolderRegular::looking for " + nameOrPath);
 		// Log.info("AParentFolderRegular::dir = " + dir);
-		if (dir == null) {
+		if (dir == null)
 			filecurrent = new SFile(nameOrPath);
-		} else {
+		else
 			filecurrent = dir.getAbsoluteFile().file(nameOrPath);
-		}
+
 		// Log.info("AParentFolderRegular::Filecurrent " + filecurrent);
-		if (filecurrent.exists()) {
+		if (filecurrent.exists())
 			return new AFileRegular(filecurrent.getCanonicalFile());
-		}
+
 		return null;
 	}
 
 }
diff --git a/src/net/sourceforge/plantuml/filesdiagram/FilesEntry.java b/src/net/sourceforge/plantuml/filesdiagram/FilesEntry.java
new file mode 100644
index 000000000..744d49514
--- /dev/null
+++ b/src/net/sourceforge/plantuml/filesdiagram/FilesEntry.java
@@ -0,0 +1,111 @@
+/* ========================================================================
+ * PlantUML : a free UML diagram generator
+ * ========================================================================
+ *
+ * (C) Copyright 2009-2024, Arnaud Roques
+ *
+ * Project Info:  https://plantuml.com
+ * 
+ * If you like this project or if you find it useful, you can support us at:
+ * 
+ * https://plantuml.com/patreon (only 1$ per month!)
+ * https://plantuml.com/paypal
+ * 
+ * This file is part of PlantUML.
+ *
+ * PlantUML is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PlantUML distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
+ * USA.
+ *
+ *
+ * Original Author:  Arnaud Roques
+ *
+ */
+package net.sourceforge.plantuml.filesdiagram;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import net.sourceforge.plantuml.klimt.UTranslate;
+import net.sourceforge.plantuml.klimt.creole.Display;
+import net.sourceforge.plantuml.klimt.drawing.UGraphic;
+import net.sourceforge.plantuml.klimt.font.FontConfiguration;
+import net.sourceforge.plantuml.klimt.geom.HorizontalAlignment;
+import net.sourceforge.plantuml.klimt.shape.TextBlock;
+import net.sourceforge.plantuml.style.ISkinParam;
+
+public class FilesEntry implements Iterable<FilesEntry> {
+
+	private final String name;
+	private FilesType type;
+	private List<FilesEntry> children = new ArrayList<>();
+
+	public FilesEntry(String name, FilesType type) {
+		this.name = name;
+		this.type = type;
+	}
+
+	public FilesEntry addRawEntry(String raw) {
+		final int x = raw.indexOf('/');
+		if (x == -1) {
+			final FilesEntry result = new FilesEntry(raw, FilesType.DATA);
+			children.add(result);
+			return result;
+		}
+		final FilesEntry folder = getOrCreateFolder(raw.substring(0, x));
+		final String remain = raw.substring(x + 1);
+		if (remain.length() == 0)
+			return folder;
+		return folder.addRawEntry(remain);
+	}
+
+	private FilesEntry getOrCreateFolder(String folderName) {
+		for (FilesEntry child : children)
+			if (child.type == FilesType.FOLDER && child.getName().equals(folderName))
+				return child;
+
+		final FilesEntry result = new FilesEntry(folderName, FilesType.FOLDER);
+		children.add(result);
+		return result;
+	}
+
+	@Override
+	public Iterator<FilesEntry> iterator() {
+		return Collections.unmodifiableCollection(children).iterator();
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	public String getEmoticon() {
+		if (type == FilesType.FOLDER)
+			return "<:1f4c2:>";
+		// return "<:1f4c1:>";
+		return "<:1f4c4:>";
+	}
+
+	public UGraphic drawAndMove(UGraphic ug, FontConfiguration fontConfiguration, ISkinParam skinParam, double deltax) {
+		final Display display = Display.getWithNewlines(getEmoticon() + getName());
+		TextBlock result = display.create(fontConfiguration, HorizontalAlignment.LEFT, skinParam);
+		result.drawU(ug.apply(UTranslate.dx(deltax)));
+		ug = ug.apply(UTranslate.dy(result.calculateDimension(ug.getStringBounder()).getHeight() + 2));
+		for (FilesEntry child : children)
+			ug = child.drawAndMove(ug, fontConfiguration, skinParam, deltax + 21);
+		return ug;
+	}
+
+}
diff --git a/src/net/sourceforge/plantuml/filesdiagram/FilesListing.java b/src/net/sourceforge/plantuml/filesdiagram/FilesListing.java
index 99be97712..097e93e69 100644
--- a/src/net/sourceforge/plantuml/filesdiagram/FilesListing.java
+++ b/src/net/sourceforge/plantuml/filesdiagram/FilesListing.java
@@ -32,30 +32,23 @@
  * Original Author:  Arnaud Roques
  *
  */
 package net.sourceforge.plantuml.filesdiagram;
 
-import java.util.ArrayList;
-import java.util.List;
-
-import net.sourceforge.plantuml.klimt.UTranslate;
-import net.sourceforge.plantuml.klimt.creole.Display;
 import net.sourceforge.plantuml.klimt.drawing.UGraphic;
 import net.sourceforge.plantuml.klimt.font.FontConfiguration;
 import net.sourceforge.plantuml.klimt.font.StringBounder;
 import net.sourceforge.plantuml.klimt.font.UFont;
-import net.sourceforge.plantuml.klimt.geom.HorizontalAlignment;
 import net.sourceforge.plantuml.klimt.geom.XDimension2D;
 import net.sourceforge.plantuml.klimt.shape.AbstractTextBlock;
-import net.sourceforge.plantuml.klimt.shape.TextBlock;
 import net.sourceforge.plantuml.style.ISkinParam;
 
 public class FilesListing extends AbstractTextBlock {
 
 	private final ISkinParam skinParam;
 	private final FontConfiguration fontConfiguration = FontConfiguration.blackBlueTrue(UFont.courier(14));
-	private final List<String> tmp = new ArrayList<>();
+	private final FilesEntry root = new FilesEntry("", FilesType.FOLDER);
 
 	public FilesListing(ISkinParam skinParam) {
 		this.skinParam = skinParam;
 	}
 
@@ -64,20 +57,15 @@ public class FilesListing extends AbstractTextBlock {
 		return new XDimension2D(200, 200);
 	}
 
 	@Override
 	public void drawU(UGraphic ug) {
-		for (String s : tmp) {
-			final Display display = Display.getWithNewlines("<:1f4c4:>" + s);
-			TextBlock result = display.create(fontConfiguration, HorizontalAlignment.LEFT, skinParam);
-			result.drawU(ug);
-			ug = ug.apply(UTranslate.dy(result.calculateDimension(ug.getStringBounder()).getHeight()));
-		}
-
+		for (FilesEntry ent : root)
+			ug = ent.drawAndMove(ug, fontConfiguration, skinParam, 0);
 	}
 
 	public void add(String line) {
 		if (line.startsWith("/"))
-			tmp.add(line.substring(1));
+			root.addRawEntry(line.substring(1));
 	}
 
 }
diff --git a/src/net/sourceforge/plantuml/filesdiagram/FilesType.java b/src/net/sourceforge/plantuml/filesdiagram/FilesType.java
new file mode 100644
index 000000000..4c6d98615
--- /dev/null
+++ b/src/net/sourceforge/plantuml/filesdiagram/FilesType.java
@@ -0,0 +1,40 @@
+/* ========================================================================
+ * PlantUML : a free UML diagram generator
+ * ========================================================================
+ *
+ * (C) Copyright 2009-2024, Arnaud Roques
+ *
+ * Project Info:  https://plantuml.com
+ * 
+ * If you like this project or if you find it useful, you can support us at:
+ * 
+ * https://plantuml.com/patreon (only 1$ per month!)
+ * https://plantuml.com/paypal
+ * 
+ * This file is part of PlantUML.
+ *
+ * PlantUML is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PlantUML distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
+ * USA.
+ *
+ *
+ * Original Author:  Arnaud Roques
+ *
+ */
+package net.sourceforge.plantuml.filesdiagram;
+
+public enum FilesType {
+	FOLDER, DATA;
+
+}
diff --git a/src/net/sourceforge/plantuml/klimt/sprite/SpriteColor.java b/src/net/sourceforge/plantuml/klimt/sprite/SpriteColor.java
index fbd3d4eb3..eba27c816 100644
--- a/src/net/sourceforge/plantuml/klimt/sprite/SpriteColor.java
+++ b/src/net/sourceforge/plantuml/klimt/sprite/SpriteColor.java
@@ -64,30 +64,30 @@ public class SpriteColor implements Sprite {
 		this.gray = new int[height][width];
 		this.color = new int[height][width];
 	}
 
 	public void setGray(int x, int y, int level) {
-		if (x < 0 || x >= width) {
+		if (x < 0 || x >= width)
 			return;
-		}
-		if (y < 0 || y >= height) {
+
+		if (y < 0 || y >= height)
 			return;
-		}
-		if (level < 0 || level >= 16) {
+
+		if (level < 0 || level >= 16)
 			throw new IllegalArgumentException();
-		}
+
 		gray[y][x] = level;
 		color[y][x] = -1;
 	}
 
 	public void setColor(int x, int y, int col) {
-		if (x < 0 || x >= width) {
+		if (x < 0 || x >= width)
 			return;
-		}
-		if (y < 0 || y >= height) {
+
+		if (y < 0 || y >= height)
 			return;
-		}
+
 		gray[y][x] = -1;
 		color[y][x] = col;
 	}
 
 	public int getHeight() {
@@ -97,18 +97,18 @@ public class SpriteColor implements Sprite {
 	public int getWidth() {
 		return width;
 	}
 
 	public UImage toUImage(ColorMapper colorMapper, HColor backcolor, HColor forecolor) {
-		final BufferedImage im = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+		final BufferedImage im = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
 
-		if (backcolor == null) {
+		if (backcolor == null)
 			backcolor = HColors.WHITE;
-		}
-		if (forecolor == null) {
+
+		if (forecolor == null)
 			forecolor = HColors.BLACK;
-		}
+
 		final HColorGradient gradient = HColors.gradient(backcolor, forecolor, '\0');
 		for (int col = 0; col < width; col++) {
 			for (int line = 0; line < height; line++) {
 				final int localColor = color[line][col];
 				if (localColor == -1) {
diff --git a/src/net/sourceforge/plantuml/nwdiag/NwDiagram.java b/src/net/sourceforge/plantuml/nwdiag/NwDiagram.java
index 2df339cd8..6bc30d64e 100644
--- a/src/net/sourceforge/plantuml/nwdiag/NwDiagram.java
+++ b/src/net/sourceforge/plantuml/nwdiag/NwDiagram.java
@@ -148,15 +148,31 @@ public class NwDiagram extends UmlDiagram {
 			return CommandExecutionResult.error("Cannot open network in a group");
 
 		for (NStackable element : stack)
 			if (element instanceof Network)
 				return CommandExecutionResult.error("Cannot nest network");
+
+		if (networks.size() == 0 && groups.size() == 0)
+			eventuallyConnectAllStandaloneServersToHiddenNetwork();
+
 		final Network network = createNetwork(name);
 		stack.add(0, network);
 		return CommandExecutionResult.ok();
 	}
 
+	private void eventuallyConnectAllStandaloneServersToHiddenNetwork() {
+		Network first = null;
+		for (NServer server : servers.values())
+			if (server.isAlone()) {
+				if (first == null) {
+					first = createNetwork("");
+					first.goInvisible();
+				}
+				server.connectMeIfAlone(first);
+			}
+	}
+
 	public CommandExecutionResult closeSomething() {
 		if (initDone == false)
 			return errorNoInit();
 
 		if (stack.size() > 0)
diff --git a/src/net/sourceforge/plantuml/nwdiag/core/NServer.java b/src/net/sourceforge/plantuml/nwdiag/core/NServer.java
index 9bba897a5..13265a2b7 100644
--- a/src/net/sourceforge/plantuml/nwdiag/core/NServer.java
+++ b/src/net/sourceforge/plantuml/nwdiag/core/NServer.java
@@ -110,10 +110,12 @@ public class NServer {
 			connections.put(it, "");
 		}
 	}
 
 	public void learnThisAddress(String address) {
+		if (address == null)
+			address = "";
 		for (Entry<Network, String> ent : connections.entrySet()) {
 			if (ent.getValue().length() == 0) {
 				connections.put(ent.getKey(), address);
 				return;
 			}
diff --git a/src/net/sourceforge/plantuml/security/SURL.java b/src/net/sourceforge/plantuml/security/SURL.java
index 5bcff360d..019915851 100644
--- a/src/net/sourceforge/plantuml/security/SURL.java
+++ b/src/net/sourceforge/plantuml/security/SURL.java
@@ -248,11 +248,11 @@ public class SURL {
 
 	// ::comment when __CORE__
 	/**
 	 * Regex to remove the UserInfo part from a URL.
 	 */
-	private static final Pattern PATTERN_USERINFO = Pattern.compile("(^https?://)([-_0-9a-zA-Z]+@)([^@]*)");
+	private static final Pattern PATTERN_USERINFO = Pattern.compile("(^https?://)([-_0-9a-zA-Z]+@)([^@]*)$");
 
 	private static final ExecutorService EXE = Executors.newCachedThreadPool(new ThreadFactory() {
 		public Thread newThread(Runnable r) {
 			final Thread t = Executors.defaultThreadFactory().newThread(r);
 			t.setDaemon(true);
@@ -290,10 +290,13 @@ public class SURL {
 	public String toString() {
 		return internal.toString();
 	}
 
 	private boolean forbiddenURL(String full) {
+		// Thanks to Agasthya Kasturi
+		if (full.contains("@"))
+			return true;
 		if (full.startsWith("https://") == false && full.startsWith("http://") == false)
 			return true;
 		if (full.matches("^https?://[-#.0-9:\\[\\]+]+/.*"))
 			return true;
 		if (full.matches("^https?://[^.]+/.*"))
@@ -303,10 +306,13 @@ public class SURL {
 		return false;
 	}
 
 	private boolean isInUrlAllowList() {
 		final String full = cleanPath(internal.toString());
+		// Thanks to Agasthya Kasturi
+		if (full.contains("@"))
+			return false;
 		for (String allow : getUrlAllowList())
 			if (full.startsWith(cleanPath(allow)))
 				return true;
 
 		return false;
diff --git a/src/net/sourceforge/plantuml/tim/TFunctionImpl.java b/src/net/sourceforge/plantuml/tim/TFunctionImpl.java
index 86417856e..dbdf7eb1a 100644
--- a/src/net/sourceforge/plantuml/tim/TFunctionImpl.java
+++ b/src/net/sourceforge/plantuml/tim/TFunctionImpl.java
@@ -58,41 +58,40 @@ public class TFunctionImpl implements TFunction {
 	private boolean containsReturn;
 
 	public TFunctionImpl(String functionName, List<TFunctionArgument> args, boolean unquoted,
 			TFunctionType functionType) {
 		final Set<String> names = new HashSet<>();
-		for (TFunctionArgument tmp : args) {
+		for (TFunctionArgument tmp : args)
 			names.add(tmp.getName());
-		}
+
 		this.signature = new TFunctionSignature(functionName, args.size(), names);
 		this.args = args;
 		this.unquoted = unquoted;
 		this.functionType = functionType;
 	}
 
 	public boolean canCover(int nbArg, Set<String> namedArguments) {
-		for (String n : namedArguments) {
-			if (signature.getNamedArguments().contains(n) == false) {
+		for (String n : namedArguments)
+			if (signature.getNamedArguments().contains(n) == false)
 				return false;
-			}
-		}
-		if (nbArg > args.size()) {
+
+		if (nbArg > args.size())
 			return false;
-		}
+
 		assert nbArg <= args.size();
 		int neededArgument = 0;
 		for (TFunctionArgument arg : args) {
-			if (namedArguments.contains(arg.getName())) {
+			if (namedArguments.contains(arg.getName()))
 				continue;
-			}
-			if (arg.getOptionalDefaultValue() == null) {
+
+			if (arg.getOptionalDefaultValue() == null)
 				neededArgument++;
-			}
+
 		}
-		if (nbArg < neededArgument) {
+		if (nbArg < neededArgument)
 			return false;
-		}
+
 		assert nbArg >= neededArgument;
 		return true;
 	}
 
 	private TMemory getNewMemory(TMemory memory, List<TValue> values, Map<String, TValue> namedArguments) {
@@ -106,13 +105,13 @@ public class TFunctionImpl implements TFunction {
 				value = values.get(ivalue);
 				ivalue++;
 			} else {
 				value = arg.getOptionalDefaultValue();
 			}
-			if (value == null) {
+			if (value == null)
 				throw new IllegalStateException();
-			}
+
 			result.put(arg.getName(), value);
 		}
 		return memory.forkFromGlobal(result);
 	}
 
@@ -123,15 +122,13 @@ public class TFunctionImpl implements TFunction {
 
 	public void addBody(StringLocated s) throws EaterExceptionLocated {
 		body.add(s);
 		if (s.getType() == TLineType.RETURN) {
 			this.containsReturn = true;
-			if (functionType == TFunctionType.PROCEDURE) {
+			if (functionType == TFunctionType.PROCEDURE)
 				throw EaterExceptionLocated
 						.located("A procedure cannot have !return directive. Declare it as a function instead ?", s);
-				// this.functionType = TFunctionType.RETURN;
-			}
 		}
 	}
 
 	public void executeProcedure(TContext context, TMemory memory, LineLocation location, String s)
 			throws EaterException, EaterExceptionLocated {
@@ -145,43 +142,43 @@ public class TFunctionImpl implements TFunction {
 		context.appendEndOfLine(endOfLine);
 	}
 
 	public void executeProcedureInternal(TContext context, TMemory memory, List<TValue> args, Map<String, TValue> named)
 			throws EaterException, EaterExceptionLocated {
-		if (functionType != TFunctionType.PROCEDURE && functionType != TFunctionType.LEGACY_DEFINELONG) {
+		if (functionType != TFunctionType.PROCEDURE && functionType != TFunctionType.LEGACY_DEFINELONG)
 			throw new IllegalStateException();
-		}
+
 		final TMemory copy = getNewMemory(memory, args, named);
 		context.executeLines(copy, body, TFunctionType.PROCEDURE, false);
 	}
 
 	public TValue executeReturnFunction(TContext context, TMemory memory, LineLocation location, List<TValue> args,
 			Map<String, TValue> named) throws EaterException, EaterExceptionLocated {
-		if (functionType == TFunctionType.LEGACY_DEFINE) {
+		if (functionType == TFunctionType.LEGACY_DEFINE)
 			return executeReturnLegacyDefine(location, context, memory, args);
-		}
-		if (functionType != TFunctionType.RETURN_FUNCTION) {
+
+		if (functionType != TFunctionType.RETURN_FUNCTION)
 			throw EaterException.unlocated("Illegal call here. Is there a return directive in your function?");
-		}
+
 		final TMemory copy = getNewMemory(memory, args, named);
 		final TValue result = context.executeLines(copy, body, TFunctionType.RETURN_FUNCTION, true);
-		if (result == null) {
+		if (result == null)
 			throw EaterException.unlocated("No return directive found in your function");
-		}
+
 		return result;
 	}
 
 	private TValue executeReturnLegacyDefine(LineLocation location, TContext context, TMemory memory, List<TValue> args)
 			throws EaterException, EaterExceptionLocated {
-		if (legacyDefinition == null) {
+		if (legacyDefinition == null)
 			throw new IllegalStateException();
-		}
+
 		final TMemory copy = getNewMemory(memory, args, Collections.<String, TValue>emptyMap());
 		final String tmp = context.applyFunctionsAndVariables(copy, location, legacyDefinition);
-		if (tmp == null) {
+		if (tmp == null)
 			return TValue.fromString("");
-		}
+
 		return TValue.fromString(tmp);
 		// eaterReturn.execute(context, copy);
 		// // System.err.println("s3=" + eaterReturn.getValue2());
 		// return eaterReturn.getValue2();
 	}
@@ -209,13 +206,13 @@ public class TFunctionImpl implements TFunction {
 	public boolean hasBody() {
 		return body.size() > 0;
 	}
 
 	public void finalizeEnddefinelong() {
-		if (functionType != TFunctionType.LEGACY_DEFINELONG) {
+		if (functionType != TFunctionType.LEGACY_DEFINELONG)
 			throw new UnsupportedOperationException();
-		}
+
 		if (body.size() == 1) {
 			this.functionType = TFunctionType.LEGACY_DEFINE;
 			this.legacyDefinition = body.get(0).getString();
 		}
 	}
diff --git a/src/net/sourceforge/plantuml/version/LicenseInfo.java b/src/net/sourceforge/plantuml/version/LicenseInfo.java
index a942f267a..bc8af9922 100644
--- a/src/net/sourceforge/plantuml/version/LicenseInfo.java
+++ b/src/net/sourceforge/plantuml/version/LicenseInfo.java
@@ -106,31 +106,31 @@ public class LicenseInfo {
 		return retrieveQuick().isValid();
 	}
 
 	public static synchronized LicenseInfo retrieveNamedSlow() {
 		cache = LicenseInfo.NONE;
-		if (OptionFlags.ALLOW_INCLUDE == false) {
+		if (OptionFlags.ALLOW_INCLUDE == false)
 			return cache;
-		}
+
 		final String key = prefs.get("license", "");
 		if (key.length() > 0) {
 			cache = setIfValid(retrieveNamed(key), cache);
-			if (cache.isValid()) {
+			if (cache.isValid())
 				return cache;
-			}
+
 		}
 		for (SFile f : fileCandidates()) {
 			try {
 				if (f.exists() && f.canRead()) {
 					final LicenseInfo result = retrieve(f);
-					if (result == null) {
+					if (result == null)
 						return null;
-					}
+
 					cache = setIfValid(result, cache);
-					if (cache.isValid()) {
+					if (cache.isValid())
 						return cache;
-					}
+
 				}
 			} catch (IOException e) {
 				Log.info("Error " + e);
 				// Logme.error(e);
 			}
@@ -155,17 +155,17 @@ public class LicenseInfo {
 		if (licenseInfo.getLicenseType() != LicenseType.DISTRIBUTOR) {
 			return null;
 		}
 		try {
 			final byte[] s1 = PLSSignature.retrieveDistributorImageSignature();
-			if (SignatureUtils.toHexString(s1).equals(SignatureUtils.toHexString(licenseInfo.sha)) == false) {
+			if (SignatureUtils.toHexString(s1).equals(SignatureUtils.toHexString(licenseInfo.sha)) == false)
 				return null;
-			}
+
 			final InputStream dis = PSystemVersion.class.getResourceAsStream("/distributor.png");
-			if (dis == null) {
+			if (dis == null)
 				return null;
-			}
+
 			try {
 				final BufferedImage result = SImageIO.read(dis);
 				return result;
 			} finally {
 				dis.close();
@@ -176,25 +176,24 @@ public class LicenseInfo {
 		return null;
 	}
 
 	public static LicenseInfo retrieveDistributor() {
 		final InputStream dis = PSystemVersion.class.getResourceAsStream("/distributor.txt");
-		if (dis == null) {
+		if (dis == null)
 			return null;
-		}
+
 		try {
 			final BufferedReader br = new BufferedReader(new InputStreamReader(dis));
 			final String licenseString = br.readLine();
 			br.close();
 			final LicenseInfo result = PLSSignature.retrieveDistributor(licenseString);
 			final Throwable creationPoint = new Throwable();
 			creationPoint.fillInStackTrace();
-			for (StackTraceElement ste : creationPoint.getStackTrace()) {
-				if (ste.toString().contains(result.context)) {
+			for (StackTraceElement ste : creationPoint.getStackTrace())
+				if (ste.toString().contains(result.context))
 					return result;
-				}
-			}
+
 			return null;
 		} catch (Exception e) {
 			Logme.error(e);
 			return null;
 		}
@@ -206,38 +205,38 @@ public class LicenseInfo {
 		String[] classpathEntries = classpath.split(SFile.pathSeparator);
 		for (String s : classpathEntries) {
 			if (s == null)
 				continue;
 			SFile dir = new SFile(s);
-			if (dir.isFile()) {
+			if (dir.isFile())
 				dir = dir.getParentFile();
-			}
-			if (dir != null && dir.isDirectory()) {
+
+			if (dir != null && dir.isDirectory())
 				result.add(dir.file("license.txt"));
-			}
+
 		}
 		return result;
 	}
 
 	private static LicenseInfo setIfValid(LicenseInfo value, LicenseInfo def) {
-		if (value.isValid() || def.isNone()) {
+		if (value.isValid() || def.isNone())
 			return value;
-		}
+
 		return def;
 	}
 
 	private static LicenseInfo retrieve(SFile f) throws IOException {
 		final BufferedReader br = f.openBufferedReader();
-		if (br == null) {
+		if (br == null)
 			return null;
-		}
+
 		try {
 			final String s = br.readLine();
 			final LicenseInfo result = retrieveNamed(s);
-			if (result != null) {
+			if (result != null)
 				Log.info("Reading license from " + f.getAbsolutePath());
-			}
+
 			return result;
 		} finally {
 			br.close();
 		}
 	}
diff --git a/src/net/sourceforge/plantuml/version/Version.java b/src/net/sourceforge/plantuml/version/Version.java
index 6eec116d6..bb24e3520 100644
--- a/src/net/sourceforge/plantuml/version/Version.java
+++ b/src/net/sourceforge/plantuml/version/Version.java
@@ -44,11 +44,11 @@ import net.sourceforge.plantuml.utils.SignatureUtils;
 public class Version {
 	// ::remove folder when __HAXE__
 
 	// Warning, "version" should be the same in gradle.properties and Version.java
 	// Any idea anyone how to magically synchronize those :-) ?
-	private static final String version = "1.2023.9beta3";
+	private static final String version = "1.2023.9beta4";
 
 	public static String versionString() {
 		return version;
 	}
 

Vulnerable code: version = 1.2023.9beta3
		if (dir == null) {
		} else {
		}
		if (filecurrent.exists()) {
		}
import java.util.ArrayList;
import java.util.List;

import net.sourceforge.plantuml.klimt.UTranslate;
import net.sourceforge.plantuml.klimt.creole.Display;
import net.sourceforge.plantuml.klimt.geom.HorizontalAlignment;
import net.sourceforge.plantuml.klimt.shape.TextBlock;
	private final List<String> tmp = new ArrayList<>();
		for (String s : tmp) {
			final Display display = Display.getWithNewlines("<:1f4c4:>" + s);
			TextBlock result = display.create(fontConfiguration, HorizontalAlignment.LEFT, skinParam);
			result.drawU(ug);
			ug = ug.apply(UTranslate.dy(result.calculateDimension(ug.getStringBounder()).getHeight()));
		}

			tmp.add(line.substring(1));
		if (x < 0 || x >= width) {
		}
		if (y < 0 || y >= height) {
		}
		if (level < 0 || level >= 16) {
		}
		if (x < 0 || x >= width) {
		}
		if (y < 0 || y >= height) {
		}
		final BufferedImage im = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
		if (backcolor == null) {
		}
		if (forecolor == null) {
		}
	private static final Pattern PATTERN_USERINFO = Pattern.compile("(^https?://)([-_0-9a-zA-Z]+@)([^@]*)");
		for (TFunctionArgument tmp : args) {
		}
		for (String n : namedArguments) {
			if (signature.getNamedArguments().contains(n) == false) {
			}
		}
		if (nbArg > args.size()) {
		}
			if (namedArguments.contains(arg.getName())) {
			}
			if (arg.getOptionalDefaultValue() == null) {
			}
		if (nbArg < neededArgument) {
		}
			if (value == null) {
			}
			if (functionType == TFunctionType.PROCEDURE) {
				// this.functionType = TFunctionType.RETURN;
			}
		if (functionType != TFunctionType.PROCEDURE && functionType != TFunctionType.LEGACY_DEFINELONG) {
		}
		if (functionType == TFunctionType.LEGACY_DEFINE) {
		}
		if (functionType != TFunctionType.RETURN_FUNCTION) {
		}
		if (result == null) {
		}
		if (legacyDefinition == null) {
		}
		if (tmp == null) {
		}
		if (functionType != TFunctionType.LEGACY_DEFINELONG) {
		}
		if (OptionFlags.ALLOW_INCLUDE == false) {
		}
			if (cache.isValid()) {
			}
					if (result == null) {
					}
					if (cache.isValid()) {
					}
			if (SignatureUtils.toHexString(s1).equals(SignatureUtils.toHexString(licenseInfo.sha)) == false) {
			}
			if (dis == null) {
			}
		if (dis == null) {
		}
			for (StackTraceElement ste : creationPoint.getStackTrace()) {
				if (ste.toString().contains(result.context)) {
				}
			}
			if (dir.isFile()) {
			}
			if (dir != null && dir.isDirectory()) {
			}
		if (value.isValid() || def.isNone()) {
		}
		if (br == null) {
		}
			if (result != null) {
			}
	private static final String version = "1.2023.9beta3";
Fixed code: version = 1.2023.9beta4
		if (dir == null)
		else

		if (filecurrent.exists())

/* ========================================================================
 * PlantUML : a free UML diagram generator
 * ========================================================================
 *
 * (C) Copyright 2009-2024, Arnaud Roques
 *
 * Project Info:  https://plantuml.com
 * 
 * If you like this project or if you find it useful, you can support us at:
 * 
 * https://plantuml.com/patreon (only 1$ per month!)
 * https://plantuml.com/paypal
 * 
 * This file is part of PlantUML.
 *
 * PlantUML is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * PlantUML distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
 * License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
 * USA.
 *
 *
 * Original Author:  Arnaud Roques
 *
 */
package net.sourceforge.plantuml.filesdiagram;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import net.sourceforge.plantuml.klimt.UTranslate;
import net.sourceforge.plantuml.klimt.creole.Display;
import net.sourceforge.plantuml.klimt.drawing.UGraphic;
import net.sourceforge.plantuml.klimt.font.FontConfiguration;
import net.sourceforge.plantuml.klimt.geom.HorizontalAlignment;
import net.sourceforge.plantuml.klimt.shape.TextBlock;
import net.sourceforge.plantuml.style.ISkinParam;

public class FilesEntry implements Iterable<FilesEntry> {

	private final String name;
	private FilesType type;
	private List<FilesEntry> children = new ArrayList<>();

	public FilesEntry(String name, FilesType type) {
		this.name = name;
		this.type = type;
	}

	public FilesEntry addRawEntry(String raw) {
		final int x = raw.indexOf('/');
		if (x == -1) {
			final FilesEntry result = new FilesEntry(raw, FilesType.DATA);
			children.add(result);
			return result;
		}
		final FilesEntry folder = getOrCreateFolder(raw.substring(0, x));
		final String remain = raw.substring(x + 1);
		if (remain.length() == 0)
			return folder;
		return folder.addRawEntry(remain);
	}

	private FilesEntry getOrCreateFolder(String folderName) {
		for (FilesEntry child : children)
			if (child.type == FilesType.FOLDER && child.getName().equals(folderName))
				return child;

		final FilesEntry result = new FilesEntry(folderName, FilesType.FOLDER);
		children.add(result);
		return result;
	}

	@Override
	public Iterator<FilesEntry> iterator() {
		return Collections.unmodifiableCollection(children).iterator();
	}

	public String getName() {
		return name;
	}

	public String getEmoticon() {
		if (type == FilesType.FOLDER)
			return "<:1f4c2:>";
		// return "<:1f4c1:>";
		return "<:1f4c4:>";
	}

	public UGraphic drawAndMove(UGraphic ug, FontConfiguration fontConfiguration, ISkinParam skinParam, double deltax) {
		final Display display = Display.getWithNewlines(getEmoticon() + getName());
		TextBlock result = display.create(fontConfiguration, HorizontalAlignment.LEFT, skinParam);
		result.drawU(ug.apply(UTranslate.dx(deltax)));
		ug = ug.apply(UTranslate.dy(result.calculateDimension(ug.getStringBounder()).getHeight() + 2));
		for (FilesEntry child : children)
			ug = child.drawAndMove(ug, fontConfiguration, skinParam, deltax + 21);
		return ug;
	}

}
	private final FilesEntry root = new FilesEntry("", FilesType.FOLDER);
		for (FilesEntry ent : root)
			ug = ent.drawAndMove(ug, fontConfiguration, skinParam, 0);
			root.addRawEntry(line.substring(1));
/* ========================================================================
 * PlantUML : a free UML diagram generator
 * ========================================================================
 *
 * (C) Copyright 2009-2024, Arnaud Roques
 *
 * Project Info:  https://plantuml.com
 * 
 * If you like this project or if you find it useful, you can support us at:
 * 
 * https://plantuml.com/patreon (only 1$ per month!)
 * https://plantuml.com/paypal
 * 
 * This file is part of PlantUML.
 *
 * PlantUML is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * PlantUML distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
 * License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
 * USA.
 *
 *
 * Original Author:  Arnaud Roques
 *
 */
package net.sourceforge.plantuml.filesdiagram;

public enum FilesType {
	FOLDER, DATA;

}
		if (x < 0 || x >= width)

		if (y < 0 || y >= height)

		if (level < 0 || level >= 16)

		if (x < 0 || x >= width)

		if (y < 0 || y >= height)

		final BufferedImage im = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
		if (backcolor == null)

		if (forecolor == null)


		if (networks.size() == 0 && groups.size() == 0)
			eventuallyConnectAllStandaloneServersToHiddenNetwork();

	private void eventuallyConnectAllStandaloneServersToHiddenNetwork() {
		Network first = null;
		for (NServer server : servers.values())
			if (server.isAlone()) {
				if (first == null) {
					first = createNetwork("");
					first.goInvisible();
				}
				server.connectMeIfAlone(first);
			}
	}

		if (address == null)
			address = "";
	private static final Pattern PATTERN_USERINFO = Pattern.compile("(^https?://)([-_0-9a-zA-Z]+@)([^@]*)$");
		// Thanks to Agasthya Kasturi
		if (full.contains("@"))
			return true;
		// Thanks to Agasthya Kasturi
		if (full.contains("@"))
			return false;
		for (TFunctionArgument tmp : args)

		for (String n : namedArguments)
			if (signature.getNamedArguments().contains(n) == false)

		if (nbArg > args.size())

			if (namedArguments.contains(arg.getName()))

			if (arg.getOptionalDefaultValue() == null)

		if (nbArg < neededArgument)

			if (value == null)

			if (functionType == TFunctionType.PROCEDURE)
		if (functionType != TFunctionType.PROCEDURE && functionType != TFunctionType.LEGACY_DEFINELONG)

		if (functionType == TFunctionType.LEGACY_DEFINE)

		if (functionType != TFunctionType.RETURN_FUNCTION)

		if (result == null)

		if (legacyDefinition == null)

		if (tmp == null)

		if (functionType != TFunctionType.LEGACY_DEFINELONG)

		if (OptionFlags.ALLOW_INCLUDE == false)

			if (cache.isValid())

					if (result == null)

					if (cache.isValid())

			if (SignatureUtils.toHexString(s1).equals(SignatureUtils.toHexString(licenseInfo.sha)) == false)

			if (dis == null)

		if (dis == null)

			for (StackTraceElement ste : creationPoint.getStackTrace())
				if (ste.toString().contains(result.context))

			if (dir.isFile())

			if (dir != null && dir.isDirectory())

		if (value.isValid() || def.isNone())

		if (br == null)

			if (result != null)

	private static final String version = "1.2023.9beta4";