From 30649d3b07f7ccbc7a4043827b49c8ccf7d8d0f9 Mon Sep 17 00:00:00 2001 From: Jakub Wlodek Date: Tue, 16 Jun 2026 21:54:25 -0400 Subject: [PATCH 1/7] Updates to resolve issues with interacting with UNC paths --- .../applications/viewer3d/Viewer3dPane.java | 6 +++++- .../editor/app/DisplayEditorInstance.java | 2 +- .../builder/model/util/ModelResourceUtil.java | 8 ++++++-- .../builder/runtime/app/DisplayInfo.java | 10 +++++++++- .../filebrowser/FileBrowserApp.java | 19 ++++++++----------- .../framework/util/ResourceParser.java | 8 ++++++++ .../framework/workbench/Locations.java | 8 +++++++- .../framework/util/ResourceParserTest.java | 13 +++++++++++++ .../phoebus/ui/docking/DockItemWithInput.java | 4 +++- 9 files changed, 60 insertions(+), 18 deletions(-) diff --git a/app/3d-viewer/src/main/java/org/phoebus/applications/viewer3d/Viewer3dPane.java b/app/3d-viewer/src/main/java/org/phoebus/applications/viewer3d/Viewer3dPane.java index caa9bfb615..859805e75e 100644 --- a/app/3d-viewer/src/main/java/org/phoebus/applications/viewer3d/Viewer3dPane.java +++ b/app/3d-viewer/src/main/java/org/phoebus/applications/viewer3d/Viewer3dPane.java @@ -16,6 +16,7 @@ import java.util.logging.Logger; import org.phoebus.framework.jobs.JobManager; +import org.phoebus.framework.util.ResourceParser; import org.phoebus.ui.javafx.ImageCache; import javafx.application.Platform; @@ -95,7 +96,10 @@ public Viewer3dPane(final URI resource, final Consumer setInput) throws Exc { if (current_resource.startsWith("file:")) { - final File file = new File(URI.create(current_resource)); + final URI uri = URI.create(current_resource); + File file = ResourceParser.getFile(uri); + if (file == null) + file = new File(uri.getPath()); fileChooser.setInitialDirectory(file.getParentFile()); fileChooser.setInitialFileName(file.getName()); } diff --git a/app/display/editor/src/main/java/org/csstudio/display/builder/editor/app/DisplayEditorInstance.java b/app/display/editor/src/main/java/org/csstudio/display/builder/editor/app/DisplayEditorInstance.java index a49eefbe7f..9a38cca751 100644 --- a/app/display/editor/src/main/java/org/csstudio/display/builder/editor/app/DisplayEditorInstance.java +++ b/app/display/editor/src/main/java/org/csstudio/display/builder/editor/app/DisplayEditorInstance.java @@ -365,7 +365,7 @@ void loadDisplay(final URI resource) // Set input ASAP to prevent opening another instance for same input dock_item.setInput(resource); - final File file = new File(resource); + final File file = Objects.requireNonNull(ResourceParser.getFile(resource)); modification_marker = file.lastModified(); editor_gui.loadModel(file); diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/util/ModelResourceUtil.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/util/ModelResourceUtil.java index 4aeb22fb4e..11bd2e91d2 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/util/ModelResourceUtil.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/util/ModelResourceUtil.java @@ -399,8 +399,12 @@ public static File getFile(final URI resource) throws Exception } } // To get a file, strip query information, - // because new File("file://xxxx?with_query") will throw exception - return ResourceParser.getFile(new URI(resource.getScheme(), null, null, -1, resource.getPath(), null, null)); + // because new File("file://xxxx?with_query") will throw exception. + // Preserve host/userInfo/port so UNC/network paths like + // file://wsl.localhost/share/path are handled correctly. + return ResourceParser.getFile(new URI(resource.getScheme(), resource.getUserInfo(), + resource.getHost(), resource.getPort(), + resource.getPath(), null, null)); } /** Open a file, web location, .. diff --git a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayInfo.java b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayInfo.java index f33e31cc32..2242fe3d19 100644 --- a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayInfo.java +++ b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayInfo.java @@ -59,7 +59,15 @@ public static DisplayInfo forURI(final URI uri) // Get basic file or http 'path' from path final String path; if (uri.getScheme() == null || uri.getScheme().equals("file")) - path = uri.getPath(); + { + // Preserve URI host for UNC/network paths, e.g. + // file://wsl.localhost/share/path -> //wsl.localhost/share/path + final String host = uri.getHost(); + if (host != null && !host.isEmpty()) + path = "//" + host + uri.getPath(); + else + path = uri.getPath(); + } else { final StringBuilder buf = new StringBuilder(); diff --git a/app/filebrowser/src/main/java/org/phoebus/applications/filebrowser/FileBrowserApp.java b/app/filebrowser/src/main/java/org/phoebus/applications/filebrowser/FileBrowserApp.java index 053898261b..2a68cb5d3f 100644 --- a/app/filebrowser/src/main/java/org/phoebus/applications/filebrowser/FileBrowserApp.java +++ b/app/filebrowser/src/main/java/org/phoebus/applications/filebrowser/FileBrowserApp.java @@ -8,6 +8,7 @@ import org.phoebus.framework.preferences.Preference; import org.phoebus.framework.spi.AppInstance; import org.phoebus.framework.spi.AppResourceDescriptor; +import org.phoebus.framework.util.ResourceParser; @SuppressWarnings("nls") public class FileBrowserApp implements AppResourceDescriptor { @@ -46,17 +47,13 @@ public AppInstance create() { @Override public AppInstance create(final URI resource) { - try - { - // Remove query component since File(URI) does not accept it - URI clean = new URI(resource.getScheme(), resource.getSchemeSpecificPart(), null); - return createWithRoot(new File(clean)); - } - catch (Exception ex) - { - // Fallback: try using path string - return createWithRoot(new File(resource.getPath())); - } + // Use ResourceParser.getFile which handles UNC/network paths + // (URIs with a host/authority component) + final File file = ResourceParser.getFile(resource); + if (file != null) + return createWithRoot(file); + // Fallback: try using path string + return createWithRoot(new File(resource.getPath())); } diff --git a/core/framework/src/main/java/org/phoebus/framework/util/ResourceParser.java b/core/framework/src/main/java/org/phoebus/framework/util/ResourceParser.java index 3559ae1b44..53a97dfddd 100644 --- a/core/framework/src/main/java/org/phoebus/framework/util/ResourceParser.java +++ b/core/framework/src/main/java/org/phoebus/framework/util/ResourceParser.java @@ -159,6 +159,14 @@ public static File getFile(final URI resource) return null; // URI might be file:/some/path/file.plt?MACRO1=Value1 // Create file for just the path, not the query params. + // + // Preserve URI host for UNC/network paths, for example + // file://wsl.localhost/AlmaLinux-9/home/user/file.bob. + // Using only resource.getPath() would drop the host and create + // a wrong local path like /AlmaLinux-9/home/.. on Windows. + final String host = resource.getHost(); + if (host != null && !host.isEmpty()) + return new File("//" + host + resource.getPath()); return new File(resource.getPath()); } diff --git a/core/framework/src/main/java/org/phoebus/framework/workbench/Locations.java b/core/framework/src/main/java/org/phoebus/framework/workbench/Locations.java index a99dc3989d..708a22d022 100644 --- a/core/framework/src/main/java/org/phoebus/framework/workbench/Locations.java +++ b/core/framework/src/main/java/org/phoebus/framework/workbench/Locations.java @@ -10,8 +10,11 @@ import static org.phoebus.framework.workbench.WorkbenchPreferences.logger; import java.io.File; +import java.net.URI; import java.util.logging.Level; +import org.phoebus.framework.util.ResourceParser; + /** Information about key locations * @author Kay Kasemir */ @@ -57,7 +60,10 @@ private static void initInstall() throws Exception // Determine location of this class // During development in the IDE, it's /some/path/phoebus/core/framework/target/classes // In the product, it's /some/path/lib/framework*.jar - File path = new File(Locations.class.getProtectionDomain().getCodeSource().getLocation().toURI()); + final URI loc = Locations.class.getProtectionDomain().getCodeSource().getLocation().toURI(); + File path = ResourceParser.getFile(loc); + if (path == null) + path = new File(loc); if (path.getName().endsWith(".jar")) { // Move up from jar to /some/path diff --git a/core/framework/src/test/java/org/phoebus/framework/util/ResourceParserTest.java b/core/framework/src/test/java/org/phoebus/framework/util/ResourceParserTest.java index 3140692486..09bb4d4df4 100644 --- a/core/framework/src/test/java/org/phoebus/framework/util/ResourceParserTest.java +++ b/core/framework/src/test/java/org/phoebus/framework/util/ResourceParserTest.java @@ -106,6 +106,19 @@ public void checkFileToURI() throws Exception { } else { assertThat(uri.toString(), equalTo("file:/some/dir%20with%20space/file.abc")); } + + // UNC-style file URI must keep host when converted to File + final URI unc = createResourceURI("file://wsl.localhost/AlmaLinux-9/home/jwlodek/test.bob"); + final File unc_file = getFile(unc); + assertThat(unc_file, not(nullValue())); + assertTrue(unc_file.getPath().toLowerCase().contains("wsl.localhost")); + assertTrue(unc_file.getPath().toLowerCase().contains("almalinux-9")); + + // Query parameters do not change underlying file path + final URI unc_with_query = createResourceURI("file://wsl.localhost/AlmaLinux-9/home/jwlodek/test.bob?app=display_editor"); + final File queried = getFile(unc_with_query); + assertThat(queried, not(nullValue())); + assertThat(queried.getPath(), equalTo(unc_file.getPath())); } @Test diff --git a/core/ui/src/main/java/org/phoebus/ui/docking/DockItemWithInput.java b/core/ui/src/main/java/org/phoebus/ui/docking/DockItemWithInput.java index 9c91ec9c0a..f340d41fa2 100644 --- a/core/ui/src/main/java/org/phoebus/ui/docking/DockItemWithInput.java +++ b/core/ui/src/main/java/org/phoebus/ui/docking/DockItemWithInput.java @@ -151,7 +151,9 @@ protected void configureContextMenu(ContextMenu menu) { if (isFileResource) { final MenuItem showInFileBrowser = new MenuItem(Messages.ShowInFileBrowserApp, new ImageView(fileBrowserIcon)); showInFileBrowser.setOnAction(e -> { - ApplicationService.createInstance("file_browser", new File(input.getPath()).toURI()); + final File file = ResourceParser.getFile(input); + if (file != null) + ApplicationService.createInstance("file_browser", file.toURI()); }); name_tab.getContextMenu().getItems().add(1, showInFileBrowser); } From 12e5a5ee2e3ce14dab9d10b00e42062cf7b4c79a Mon Sep 17 00:00:00 2001 From: Jakub Wlodek Date: Tue, 16 Jun 2026 22:05:30 -0400 Subject: [PATCH 2/7] Another fix --- .../builder/model/util/ModelResourceUtil.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/util/ModelResourceUtil.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/util/ModelResourceUtil.java index 11bd2e91d2..a3931aadd1 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/util/ModelResourceUtil.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/util/ModelResourceUtil.java @@ -121,6 +121,10 @@ public static String normalize(String path) path = path.replaceAll("\\\\(?!\\\\)", "/"); + // Detect UNC paths (//host/share/...) so we can restore the + // leading "//" after Paths.get().normalize() collapses it. + final boolean isUNC = path.startsWith("//"); + // Collapse "something/../" into "something/" if(path.contains(":")){ String[] pathsplit = path.split(":"); @@ -135,7 +139,13 @@ public static String normalize(String path) // Pattern: '\(?!\)', i.e. backslash _not_ followed by another one. // Each \ is doubled as \\ to get one '\' into the string, // then doubled once more to tell regex that we want a '\' - return protocol + path.replaceAll("\\\\(?!\\\\)", "/"); + path = protocol + path.replaceAll("\\\\(?!\\\\)", "/"); + + // Restore UNC prefix if Paths.get().normalize() collapsed "//" to "/" + if (isUNC && !path.startsWith("//")) + path = "/" + path; + + return path; } /** Obtain directory of file. For URL, this is the path up to the last element From f58a92be64fd598673f952e895a7f2ccd5d99847 Mon Sep 17 00:00:00 2001 From: Jakub Wlodek Date: Tue, 16 Jun 2026 22:11:50 -0400 Subject: [PATCH 3/7] Handle path normalization on windows correctly --- .../display/builder/model/util/ModelResourceUtil.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/util/ModelResourceUtil.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/util/ModelResourceUtil.java index a3931aadd1..81c5599c8d 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/util/ModelResourceUtil.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/util/ModelResourceUtil.java @@ -141,9 +141,13 @@ public static String normalize(String path) // then doubled once more to tell regex that we want a '\' path = protocol + path.replaceAll("\\\\(?!\\\\)", "/"); - // Restore UNC prefix if Paths.get().normalize() collapsed "//" to "/" + // Restore UNC prefix after Paths.get().normalize() and backslash conversion. + // On Linux, "//host/share" normalizes to "/host/share", so need to add "/". + // On Windows, "//host/share" normalizes to "\\host\share", so regex converts + // only the second \ (not followed by \), leaving "\/host/share", so need to + // strip leading separators and re-add "//". if (isUNC && !path.startsWith("//")) - path = "/" + path; + path = "//" + path.replaceFirst("^[/\\\\]+", ""); return path; } From 7644206dc56ea92d3819fd02b1bceca3673462e5 Mon Sep 17 00:00:00 2001 From: Jakub Wlodek Date: Tue, 16 Jun 2026 22:30:42 -0400 Subject: [PATCH 4/7] Handle File.toURI edge case --- .../builder/runtime/app/DisplayInfo.java | 6 ++++ .../framework/util/ResourceParser.java | 28 ++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayInfo.java b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayInfo.java index 2242fe3d19..8887dc88be 100644 --- a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayInfo.java +++ b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayInfo.java @@ -66,7 +66,13 @@ public static DisplayInfo forURI(final URI uri) if (host != null && !host.isEmpty()) path = "//" + host + uri.getPath(); else + { + // On Windows, File.toURI() for UNC paths may produce URIs + // with the host embedded in the path (file:////host/share). + // In that case getHost() is null but getPath() starts with "//". + // Preserve the double-slash so it's treated as a UNC path. path = uri.getPath(); + } } else { diff --git a/core/framework/src/main/java/org/phoebus/framework/util/ResourceParser.java b/core/framework/src/main/java/org/phoebus/framework/util/ResourceParser.java index 53a97dfddd..c7d23a40d7 100644 --- a/core/framework/src/main/java/org/phoebus/framework/util/ResourceParser.java +++ b/core/framework/src/main/java/org/phoebus/framework/util/ResourceParser.java @@ -176,7 +176,33 @@ public static File getFile(final URI resource) */ public static URI getURI(final File file) { - return file.toURI(); + final URI uri = file.toURI(); + // On Windows, File.toURI() for UNC paths (\\host\share\path) may + // produce URIs without a proper host component, e.g. + // file:////host/share/path (host embedded in path). + // Normalize to file://host/share/path so consumers can use getHost(). + if ("file".equals(uri.getScheme()) && uri.getHost() == null) + { + final String path = uri.getPath(); + if (path != null && path.startsWith("//")) + { + final int thirdSlash = path.indexOf('/', 2); + if (thirdSlash > 2) + { + final String host = path.substring(2, thirdSlash); + final String remainder = path.substring(thirdSlash); + try + { + return new URI("file", host, remainder, null); + } + catch (Exception ex) + { + // Fall through to return original URI + } + } + } + } + return uri; } /** Open a resource that can be read (file, web link) From d56c6209ea94100f554514d2a354341044ecd5de Mon Sep 17 00:00:00 2001 From: Jakub Wlodek Date: Tue, 16 Jun 2026 22:41:30 -0400 Subject: [PATCH 5/7] Fix normalization for open display action paths --- .../display/builder/model/util/ModelResourceUtil.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/util/ModelResourceUtil.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/util/ModelResourceUtil.java index 81c5599c8d..227b774986 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/util/ModelResourceUtil.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/util/ModelResourceUtil.java @@ -119,11 +119,11 @@ public static String normalize(String path) path = splitPath[1]; } - path = path.replaceAll("\\\\(?!\\\\)", "/"); + // Detect UNC paths BEFORE backslash conversion. + // UNC paths start with "//" (Unix-style) or "\\" (Windows-style). + final boolean isUNC = path.startsWith("//") || path.startsWith("\\\\"); - // Detect UNC paths (//host/share/...) so we can restore the - // leading "//" after Paths.get().normalize() collapses it. - final boolean isUNC = path.startsWith("//"); + path = path.replaceAll("\\\\(?!\\\\)", "/"); // Collapse "something/../" into "something/" if(path.contains(":")){ From d00b25ecd1781fae2d0f5e9063a7d1f364a94dfa Mon Sep 17 00:00:00 2001 From: Jakub Wlodek Date: Tue, 16 Jun 2026 23:00:05 -0400 Subject: [PATCH 6/7] One more edge case --- .../csstudio/display/builder/runtime/app/DisplayInfo.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayInfo.java b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayInfo.java index 8887dc88be..36bf617e53 100644 --- a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayInfo.java +++ b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayInfo.java @@ -132,7 +132,12 @@ public static DisplayInfo forModel(final DisplayModel model) { String userDataInputFile = model.getUserData(DisplayModel.USER_DATA_INPUT_FILE); String userDataInputFile_lowerCase = userDataInputFile.toLowerCase(Locale.ROOT); - if ( !userDataInputFile_lowerCase.startsWith("/") + if (userDataInputFile.startsWith("\\\\")) { + // UNC path like \\wsl.localhost\share\... -> //wsl.localhost/share/... + // Don't prepend '/' — the double slash IS the leading path indicator for UNC + path = userDataInputFile.replace('\\', '/'); + } + else if ( !userDataInputFile_lowerCase.startsWith("/") && !userDataInputFile_lowerCase.startsWith("examples:") && !userDataInputFile_lowerCase.startsWith("file:") && !userDataInputFile_lowerCase.startsWith("http:") From 20983916ff69eb5447b58f345771e8cec091a1bd Mon Sep 17 00:00:00 2001 From: Jakub Wlodek Date: Tue, 16 Jun 2026 23:19:15 -0400 Subject: [PATCH 7/7] Add additional unit tests to cover UNC file handling --- .../model/util/ModelResourceUtilTest.java | 105 +++++++++++++++ .../builder/runtime/test/DisplayInfoTest.java | 122 ++++++++++++++++++ .../framework/util/ResourceParserTest.java | 56 ++++++++ 3 files changed, 283 insertions(+) diff --git a/app/display/model/src/test/java/org/csstudio/display/builder/model/util/ModelResourceUtilTest.java b/app/display/model/src/test/java/org/csstudio/display/builder/model/util/ModelResourceUtilTest.java index 6090bb8e9a..321411b80d 100644 --- a/app/display/model/src/test/java/org/csstudio/display/builder/model/util/ModelResourceUtilTest.java +++ b/app/display/model/src/test/java/org/csstudio/display/builder/model/util/ModelResourceUtilTest.java @@ -14,6 +14,7 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; /** JUnit test of the {@link ModelResourceUtil} * @author Kay Kasemir @@ -38,4 +39,108 @@ public void testExamples() throws Exception final File file = ModelResourceUtil.getFile(URI.create("examples:/monitors_textupdate.bob")); assertThat(file.canRead(), equalTo(true)); } + + @Test + public void testNormalizeUNC() throws Exception + { + // UNC path with forward slashes should be preserved + String normalized = ModelResourceUtil.normalize("//wsl.localhost/AlmaLinux-9/home/user/display.bob"); + assertTrue(normalized.startsWith("//wsl.localhost/"), + "Normalized UNC path should start with //wsl.localhost/: " + normalized); + assertTrue(normalized.endsWith("/home/user/display.bob"), + "Normalized UNC path should keep full path: " + normalized); + + // UNC path with backslashes (Windows-style) should convert to forward slashes + // and preserve the // prefix + normalized = ModelResourceUtil.normalize("\\\\wsl.localhost\\AlmaLinux-9\\home\\user\\display.bob"); + assertTrue(normalized.startsWith("//wsl.localhost/"), + "Normalized Windows UNC path should start with //: " + normalized); + assertTrue(normalized.contains("AlmaLinux-9/home/user/display.bob"), + "Normalized Windows UNC path should convert backslashes: " + normalized); + + // UNC path with ".." should collapse parent references but keep // + normalized = ModelResourceUtil.normalize("//server/share/dir/../file.bob"); + assertTrue(normalized.startsWith("//server/"), + "Normalized UNC with .. should keep //: " + normalized); + assertTrue(normalized.contains("/share/file.bob") || normalized.endsWith("/share/file.bob"), + "Normalized UNC with .. should collapse ..: " + normalized); + + // Regular absolute path should not gain a // prefix + normalized = ModelResourceUtil.normalize("/home/user/display.bob"); + assertThat(normalized, equalTo("/home/user/display.bob")); + + // Regular Windows path should not gain a // prefix + normalized = ModelResourceUtil.normalize("C:\\Users\\test\\display.bob"); + assertThat(normalized, equalTo("C:/Users/test/display.bob")); + + // URL should not be affected by UNC handling + normalized = ModelResourceUtil.normalize("http://server.example/path/display.bob"); + assertThat(normalized, equalTo("http://server.example/path/display.bob")); + } + + @Test + public void testCombineDisplayPathsUNC() throws Exception + { + // Relative display path resolved against UNC parent + final String parent = "//wsl.localhost/AlmaLinux-9/home/user/displays/main.bob"; + String combined = ModelResourceUtil.combineDisplayPaths(parent, "child.bob"); + assertTrue(combined.startsWith("//wsl.localhost/"), + "Combined with UNC parent should keep //: " + combined); + assertTrue(combined.endsWith("/displays/child.bob"), + "Combined should resolve relative child: " + combined); + + // Relative display path with subdirectory + combined = ModelResourceUtil.combineDisplayPaths(parent, "subdir/other.bob"); + assertTrue(combined.startsWith("//wsl.localhost/"), + "Combined with subdir should keep //: " + combined); + assertTrue(combined.endsWith("/displays/subdir/other.bob"), + "Combined should include subdir: " + combined); + + // Relative display path with parent reference (..) + combined = ModelResourceUtil.combineDisplayPaths(parent, "../sibling/other.bob"); + assertTrue(combined.startsWith("//wsl.localhost/"), + "Combined with .. should keep //: " + combined); + assertTrue(combined.contains("/home/user/sibling/other.bob"), + "Combined with .. should resolve correctly: " + combined); + + // Absolute display path should not be affected by parent + combined = ModelResourceUtil.combineDisplayPaths(parent, "//other.server/share/abs.bob"); + assertThat(combined, equalTo("//other.server/share/abs.bob")); + + // Null parent should just return the display path + combined = ModelResourceUtil.combineDisplayPaths(null, "//wsl.localhost/share/file.bob"); + assertThat(combined, equalTo("//wsl.localhost/share/file.bob")); + } + + @Test + public void testGetFileUNC() throws Exception + { + // getFile with UNC URI (host in authority) should produce File with host in path + final URI unc_uri = new URI("file", "wsl.localhost", "/AlmaLinux-9/home/user/display.bob", null); + final File unc_file = ModelResourceUtil.getFile(unc_uri); + assertTrue(unc_file != null); + assertTrue(unc_file.getPath().contains("wsl.localhost"), + "getFile should preserve UNC host: " + unc_file.getPath()); + + // getFile with UNC URI that also has a query parameter + final URI unc_query = new URI("file", "server.example", "/share/displays/main.bob", "X=1&Y=2"); + final File query_file = ModelResourceUtil.getFile(unc_query); + assertTrue(query_file != null); + assertTrue(query_file.getPath().contains("server.example"), + "getFile with query should preserve UNC host: " + query_file.getPath()); + // Query should be stripped from the file path + assertTrue(!query_file.getPath().contains("X=1"), + "getFile should strip query from path: " + query_file.getPath()); + } + + @Test + public void testGetDirectoryUNC() throws Exception + { + // getDirectory on a UNC path should preserve the // prefix + final String dir = ModelResourceUtil.getDirectory("//wsl.localhost/AlmaLinux-9/home/user/file.bob"); + assertTrue(dir.startsWith("//wsl.localhost/"), + "getDirectory should keep UNC //: " + dir); + assertTrue(dir.endsWith("/home/user"), + "getDirectory should return parent directory: " + dir); + } } diff --git a/app/display/runtime/src/test/java/org/csstudio/display/builder/runtime/test/DisplayInfoTest.java b/app/display/runtime/src/test/java/org/csstudio/display/builder/runtime/test/DisplayInfoTest.java index bb719d6516..f4b5cc9712 100644 --- a/app/display/runtime/src/test/java/org/csstudio/display/builder/runtime/test/DisplayInfoTest.java +++ b/app/display/runtime/src/test/java/org/csstudio/display/builder/runtime/test/DisplayInfoTest.java @@ -7,6 +7,7 @@ *******************************************************************************/ package org.csstudio.display.builder.runtime.test; +import org.csstudio.display.builder.model.DisplayModel; import org.csstudio.display.builder.runtime.app.DisplayInfo; import org.junit.jupiter.api.Test; import org.phoebus.framework.macros.Macros; @@ -17,6 +18,7 @@ import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.sameInstance; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; /** JUnit test of the {@link DisplayInfo} * @author Kay Kasemir @@ -113,4 +115,124 @@ public void testUniqueness() throws Exception assertThat(info1.toURI().toString(), equalTo(info2.toURI().toString())); assertThat(info2.toURI().toString(), equalTo("file:/some/path/xx.bob?X=Fred+Harvey+Newman&Y=2&Z=1")); } + + @Test + public void testForURI_UNC() throws Exception + { + // UNC URI with host in authority: file://wsl.localhost/share/path + final URI unc = new URI("file", "wsl.localhost", "/AlmaLinux-9/home/user/display.bob", null); + final DisplayInfo info = DisplayInfo.forURI(unc); + System.out.println("UNC forURI: " + info); + + // Path should have // prefix with host + assertThat(info.getPath(), equalTo("//wsl.localhost/AlmaLinux-9/home/user/display.bob")); + assertThat(info.getName(), equalTo("display.bob")); + + // UNC URI with macros + final URI unc_macros = new URI("file://wsl.localhost/share/path/test.bob?X=1&Y=hello"); + final DisplayInfo info_macros = DisplayInfo.forURI(unc_macros); + System.out.println("UNC forURI with macros: " + info_macros); + + assertThat(info_macros.getPath(), equalTo("//wsl.localhost/share/path/test.bob")); + assertThat(info_macros.getMacros().getValue("X"), equalTo("1")); + assertThat(info_macros.getMacros().getValue("Y"), equalTo("hello")); + + // UNC URI with no host but path starting with // (file:////host/share) + final URI unc_no_host = URI.create("file:////server/share/path/file.bob"); + final DisplayInfo info_no_host = DisplayInfo.forURI(unc_no_host); + System.out.println("UNC forURI no host: " + info_no_host); + + // getPath() on such a URI gives "//server/share/path/file.bob" + assertTrue(info_no_host.getPath().startsWith("//server/"), + "No-host UNC should preserve //: " + info_no_host.getPath()); + } + + @Test + public void testToURI_UNC() throws Exception + { + // UNC path with // prefix -> toURI should produce file://host/path + final DisplayInfo info = new DisplayInfo("//wsl.localhost/AlmaLinux-9/home/user/display.bob", + null, new Macros(), false); + final URI uri = info.toURI(); + System.out.println("UNC toURI: " + uri); + + // toURI prepends "file:" to path, so the result should be + // file://wsl.localhost/AlmaLinux-9/home/user/display.bob + assertThat(uri.toString(), equalTo("file://wsl.localhost/AlmaLinux-9/home/user/display.bob")); + assertThat(uri.getScheme(), equalTo("file")); + assertThat(uri.getHost(), equalTo("wsl.localhost")); + assertThat(uri.getPath(), equalTo("/AlmaLinux-9/home/user/display.bob")); + + // Round-trip: forURI(toURI()) should give back original path + final DisplayInfo round_trip = DisplayInfo.forURI(uri); + assertThat(round_trip.getPath(), equalTo("//wsl.localhost/AlmaLinux-9/home/user/display.bob")); + + // UNC path with macros + final Macros macros = new Macros(); + macros.add("SYS", "BL1"); + final DisplayInfo info_macros = new DisplayInfo("//server/share/displays/main.bob", + null, macros, false); + final URI uri_macros = info_macros.toURI(); + System.out.println("UNC toURI with macros: " + uri_macros); + + assertThat(uri_macros.getScheme(), equalTo("file")); + assertThat(uri_macros.getHost(), equalTo("server")); + assertTrue(uri_macros.toString().contains("SYS=BL1"), + "URI should contain macro: " + uri_macros); + + // Round-trip with macros + final DisplayInfo round_macros = DisplayInfo.forURI(uri_macros); + assertThat(round_macros.getPath(), equalTo("//server/share/displays/main.bob")); + assertThat(round_macros.getMacros().getValue("SYS"), equalTo("BL1")); + } + + @Test + public void testForModel_UNC() throws Exception + { + // Simulate what happens when a model has USER_DATA_INPUT_FILE set to a + // Windows UNC absolute path (from File.getAbsolutePath() on Windows) + final DisplayModel model = new DisplayModel(); + model.propMacros().getValue().add("DEVICE", "Motor1"); + + // Windows UNC path: \\wsl.localhost\AlmaLinux-9\home\\display.bob + model.setUserData(DisplayModel.USER_DATA_INPUT_FILE, + "\\\\wsl.localhost\\AlmaLinux-9\\home\\" + "user\\display.bob"); + DisplayInfo info = DisplayInfo.forModel(model); + System.out.println("forModel UNC backslash: " + info); + + // Should produce //wsl.localhost/... (not /wsl.localhost/...) + assertThat(info.getPath(), equalTo("//wsl.localhost/AlmaLinux-9/home/user/display.bob")); + assertThat(info.getMacros().getValue("DEVICE"), equalTo("Motor1")); + + // Verify full round-trip: forModel -> toURI -> forURI gives same path + final URI uri = info.toURI(); + System.out.println("forModel -> toURI: " + uri); + assertThat(uri.getHost(), equalTo("wsl.localhost")); + + final DisplayInfo round_trip = DisplayInfo.forURI(uri); + assertThat(round_trip.getPath(), equalTo("//wsl.localhost/AlmaLinux-9/home/user/display.bob")); + + // Unix-style UNC path (already forward slashes) + model.setUserData(DisplayModel.USER_DATA_INPUT_FILE, + "//wsl.localhost/AlmaLinux-9/home/user/display.bob"); + info = DisplayInfo.forModel(model); + System.out.println("forModel UNC forward slash: " + info); + + // Starts with "/" so goes through else branch, should be unchanged + assertThat(info.getPath(), equalTo("//wsl.localhost/AlmaLinux-9/home/user/display.bob")); + + // Regular Windows path (not UNC) should still get "/" prepended + model.setUserData(DisplayModel.USER_DATA_INPUT_FILE, + "C:\\Users\\test\\display.bob"); + info = DisplayInfo.forModel(model); + System.out.println("forModel Windows: " + info); + assertThat(info.getPath(), equalTo("/C:/Users/test/display.bob")); + + // Regular Unix path should pass through unchanged + model.setUserData(DisplayModel.USER_DATA_INPUT_FILE, + "/home/user/displays/main.bob"); + info = DisplayInfo.forModel(model); + System.out.println("forModel Unix: " + info); + assertThat(info.getPath(), equalTo("/home/user/displays/main.bob")); + } } diff --git a/core/framework/src/test/java/org/phoebus/framework/util/ResourceParserTest.java b/core/framework/src/test/java/org/phoebus/framework/util/ResourceParserTest.java index 09bb4d4df4..c77477c9cb 100644 --- a/core/framework/src/test/java/org/phoebus/framework/util/ResourceParserTest.java +++ b/core/framework/src/test/java/org/phoebus/framework/util/ResourceParserTest.java @@ -121,6 +121,62 @@ public void checkFileToURI() throws Exception { assertThat(queried.getPath(), equalTo(unc_file.getPath())); } + @Test + public void checkUNCPaths() throws Exception + { + // UNC file URI with host -> getFile preserves host in path + final URI unc_uri = new URI("file", "wsl.localhost", "/AlmaLinux-9/home/user/display.bob", null); + assertThat(unc_uri.getHost(), equalTo("wsl.localhost")); + assertThat(unc_uri.getPath(), equalTo("/AlmaLinux-9/home/user/display.bob")); + + final File unc_file = getFile(unc_uri); + assertThat(unc_file, not(nullValue())); + // File path must contain both the host and the share path + final String unc_path = unc_file.getPath(); + assertTrue(unc_path.contains("wsl.localhost"), "UNC file path should contain host: " + unc_path); + assertTrue(unc_path.contains("AlmaLinux-9"), "UNC file path should contain share: " + unc_path); + + // getURI for a UNC File should produce a URI with proper host + final File unc_input = new File("//wsl.localhost/AlmaLinux-9/home/user/display.bob"); + final URI round_trip = getURI(unc_input); + assertThat(round_trip.getScheme(), equalTo("file")); + // On Linux, File("//host/...").toURI() may give file:///host/...; + // on Windows, it gives file:////host/.... Either way our getURI() + // should normalize it so the host is accessible: + if (round_trip.getHost() != null) + { + assertThat(round_trip.getHost(), equalTo("wsl.localhost")); + assertThat(round_trip.getPath(), equalTo("/AlmaLinux-9/home/user/display.bob")); + } + else + { + // On some platforms, the host ends up in the path as //host/path + assertTrue(round_trip.getPath().startsWith("//wsl.localhost/") + || round_trip.getPath().startsWith("/wsl.localhost/"), + "Path should contain host: " + round_trip.getPath()); + } + + // Round-trip: getFile(getURI(file)) should preserve the host + final File round_trip_file = getFile(round_trip); + assertThat(round_trip_file, not(nullValue())); + assertTrue(round_trip_file.getPath().contains("wsl.localhost"), + "Round-trip file path should contain host: " + round_trip_file.getPath()); + + // UNC URI with query parameters should not lose host + final URI unc_query = new URI("file", "server.example", "/share/path/file.bob", "app=display_runtime"); + final File unc_query_file = getFile(unc_query); + assertThat(unc_query_file, not(nullValue())); + assertTrue(unc_query_file.getPath().contains("server.example"), + "Query file path should contain host: " + unc_query_file.getPath()); + + // UNC URI with no host (path starts with //) should preserve the double slash + final URI unc_no_host = URI.create("file:////server/share/path/file.bob"); + final File unc_no_host_file = getFile(unc_no_host); + assertThat(unc_no_host_file, not(nullValue())); + assertTrue(unc_no_host_file.getPath().contains("server"), + "No-host UNC file path should contain server: " + unc_no_host_file.getPath()); + } + @Test public void checkWebToURI() throws Exception {