diff --git a/com.microsoft.java.debug.plugin/pom.xml b/com.microsoft.java.debug.plugin/pom.xml
index 2e2c0e6a..400c6882 100644
--- a/com.microsoft.java.debug.plugin/pom.xml
+++ b/com.microsoft.java.debug.plugin/pom.xml
@@ -10,6 +10,14 @@
com.microsoft.java.debug.plugin
eclipse-plugin
${base.name} :: Debugger Plugin
+
+
+ junit
+ junit
+ 4.13.1
+ test
+
+
@@ -33,6 +41,52 @@
org.apache.maven.plugins
maven-checkstyle-plugin
+
+ org.eclipse.tycho
+ target-platform-configuration
+ ${tycho-version}
+
+
+
+ eclipse-plugin
+ org.eclipse.jdt.core.compiler.batch
+
+
+ [3.44.0,3.45.0)
+
+
+
+
+
+
+ org.eclipse.tycho
+ tycho-surefire-plugin
+ ${tycho-version}
+
+ junit4
+ true
+
+
+ eclipse-plugin
+ org.eclipse.m2e.jdt
+ 0.0.0
+
+
+ eclipse-plugin
+ org.eclipse.buildship.core
+ 0.0.0
+
+
+
+
+
+
+ plugin-test
+ verify
+
+
+
+
org.apache.maven.plugins
maven-dependency-plugin
diff --git a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JdtSourceLookUpProvider.java b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JdtSourceLookUpProvider.java
index 2c39425e..75743365 100644
--- a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JdtSourceLookUpProvider.java
+++ b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JdtSourceLookUpProvider.java
@@ -11,12 +11,11 @@
package com.microsoft.java.debug.plugin.internal;
-import java.io.BufferedReader;
-import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URISyntaxException;
+import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
@@ -35,6 +34,9 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;
+import org.apache.commons.io.ByteOrderMark;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.io.input.BOMInputStream;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
@@ -312,7 +314,9 @@ private CompilationUnit asCompilationUnit(String uri) {
// For file uri, read the file contents directly and pass them to the ast
// parser.
if (filePath != null && Files.isRegularFile(Paths.get(filePath))) {
- String source = readFile(filePath);
+ IFile resource = (IFile) JDTUtils.findResource(JDTUtils.toURI(uri),
+ ResourcesPlugin.getWorkspace().getRoot()::findFilesForLocationURI);
+ String source = readFile(filePath, resource);
parser.setSource(source.toCharArray());
/**
* See the java doc for { @link ASTParser#setResolveBindings(boolean) }.
@@ -326,8 +330,6 @@ private CompilationUnit asCompilationUnit(String uri) {
* setEnvironment(String [], String [], String [], boolean)
* and a unit name setUnitName(String).
*/
- IFile resource = (IFile) JDTUtils.findResource(JDTUtils.toURI(uri),
- ResourcesPlugin.getWorkspace().getRoot()::findFilesForLocationURI);
if (resource != null && JdtUtils.isJavaProject(resource.getProject())) {
parser.setProject(JavaCore.create(resource.getProject()));
} else {
@@ -498,22 +500,21 @@ private static IClassFile resolveClassFile(String uriString) {
return null;
}
- private static String readFile(String filePath) {
- StringBuilder builder = new StringBuilder();
- try (BufferedReader bufferReader = new BufferedReader(new InputStreamReader(new FileInputStream(filePath)))) {
- final int BUFFER_SIZE = 4096;
- char[] buffer = new char[BUFFER_SIZE];
- while (true) {
- int read = bufferReader.read(buffer, 0, BUFFER_SIZE);
- if (read == -1) {
- break;
- }
- builder.append(new String(buffer, 0, read));
+ private static String readFile(String filePath, IFile resource) {
+ try {
+ if (resource != null) {
+ return resource.readString();
+ }
+ try (BOMInputStream input = BOMInputStream.builder().setPath(Paths.get(filePath))
+ .setByteOrderMarks(ByteOrderMark.UTF_8, ByteOrderMark.UTF_16LE, ByteOrderMark.UTF_16BE).get();
+ InputStreamReader reader = new InputStreamReader(input,
+ input.hasBOM() ? input.getBOMCharsetName() : Charset.defaultCharset().name())) {
+ return IOUtils.toString(reader);
}
- } catch (IOException e) {
- // do nothing.
+ } catch (IOException | CoreException e) {
+ logger.log(Level.SEVERE, "Failed to read source file: " + filePath, e);
+ return "";
}
- return builder.toString();
}
private static String resolveSystemLibraryVersion(IJavaProject project, IVMInstall vmInstall)
diff --git a/com.microsoft.java.debug.plugin/src/test/java/com/microsoft/java/debug/plugin/internal/JdtSourceLookUpProviderTest.java b/com.microsoft.java.debug.plugin/src/test/java/com/microsoft/java/debug/plugin/internal/JdtSourceLookUpProviderTest.java
new file mode 100644
index 00000000..37fc42e4
--- /dev/null
+++ b/com.microsoft.java.debug.plugin/src/test/java/com/microsoft/java/debug/plugin/internal/JdtSourceLookUpProviderTest.java
@@ -0,0 +1,142 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Microsoft Corporation and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Microsoft Corporation - initial API and implementation
+ *******************************************************************************/
+
+package com.microsoft.java.debug.plugin.internal;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IProjectDescription;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.jdt.core.IClasspathEntry;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.JavaCore;
+import org.junit.After;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+import com.microsoft.java.debug.core.JavaBreakpointLocation;
+import com.microsoft.java.debug.core.protocol.Types.SourceBreakpoint;
+
+@RunWith(Parameterized.class)
+public class JdtSourceLookUpProviderTest {
+ private enum SourceKind {
+ EXTERNAL, WORKSPACE, FILE_CHARSET, PROJECT_CHARSET
+ }
+
+ private static final String PACKAGE_NAME = "d\u00e9mo"; // A non-ASCII identifier exposes incorrect decoding.
+ private static final String SOURCE = String.join("\r\n",
+ "package " + PACKAGE_NAME + ";",
+ "",
+ "public class A {",
+ " public static void main(String[] args) {",
+ " int x = 1;",
+ " System.out.println(x);",
+ " }",
+ "}",
+ "");
+
+ @Rule
+ public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ private final Charset charset;
+ private final boolean withBom;
+ private final SourceKind sourceKind;
+ private IProject project;
+
+ public JdtSourceLookUpProviderTest(Charset charset, boolean withBom, SourceKind sourceKind) {
+ this.charset = charset;
+ this.withBom = withBom;
+ this.sourceKind = sourceKind;
+ }
+
+ @Parameters(name = "{index}: {0}, BOM={1}, {2}")
+ public static Object[][] encodings() {
+ return new Object[][] {
+ {StandardCharsets.UTF_16LE, true, SourceKind.EXTERNAL},
+ {StandardCharsets.UTF_16BE, true, SourceKind.EXTERNAL},
+ {StandardCharsets.UTF_8, true, SourceKind.EXTERNAL},
+ {StandardCharsets.UTF_8, false, SourceKind.EXTERNAL},
+ {Charset.defaultCharset(), false, SourceKind.EXTERNAL},
+ {StandardCharsets.UTF_16LE, true, SourceKind.WORKSPACE},
+ {StandardCharsets.UTF_16BE, true, SourceKind.WORKSPACE},
+ {StandardCharsets.UTF_8, true, SourceKind.WORKSPACE},
+ {StandardCharsets.UTF_16LE, false, SourceKind.FILE_CHARSET},
+ {StandardCharsets.UTF_16BE, false, SourceKind.FILE_CHARSET},
+ {StandardCharsets.UTF_8, false, SourceKind.FILE_CHARSET},
+ {StandardCharsets.ISO_8859_1, false, SourceKind.FILE_CHARSET},
+ {StandardCharsets.ISO_8859_1, false, SourceKind.PROJECT_CHARSET},
+ {StandardCharsets.UTF_16LE, false, SourceKind.PROJECT_CHARSET}
+ };
+ }
+
+ @After
+ public void deleteProject() throws Exception {
+ if (project != null && project.exists()) {
+ project.delete(true, true, null);
+ }
+ }
+
+ @Test
+ public void resolvesBreakpointClassName() throws Exception {
+ byte[] contents = (withBom ? "\uFEFF" + SOURCE : SOURCE).getBytes(charset);
+ String uri;
+ if (sourceKind == SourceKind.EXTERNAL) {
+ Path file = temporaryFolder.getRoot().toPath().resolve("A.java");
+ Files.write(file, contents);
+ uri = file.toUri().toASCIIString();
+ } else {
+ IFile file = createWorkspaceFile(contents);
+ uri = file.getLocationURI().toASCIIString();
+ }
+
+ JavaBreakpointLocation[] locations = new JdtSourceLookUpProvider().getBreakpointLocations(uri,
+ new SourceBreakpoint[] {new SourceBreakpoint(5, -1)});
+
+ assertEquals(1, locations.length);
+ assertEquals(PACKAGE_NAME + ".A", locations[0].className());
+ assertEquals(5, locations[0].lineNumber());
+ assertEquals(5, locations[0].availableBreakpointLocations()[0].line);
+ }
+
+ private IFile createWorkspaceFile(byte[] contents) throws Exception {
+ project = ResourcesPlugin.getWorkspace().getRoot().getProject("source-encoding-test");
+ project.create(null);
+ project.open(null);
+ project.setDefaultCharset(StandardCharsets.UTF_8.name(), null);
+ IProjectDescription description = project.getDescription();
+ description.setNatureIds(new String[] {JavaCore.NATURE_ID});
+ project.setDescription(description, null);
+ IJavaProject javaProject = JavaCore.create(project);
+ javaProject.setRawClasspath(new IClasspathEntry[] {JavaCore.newSourceEntry(project.getFullPath())},
+ project.getFullPath().append("bin"), null);
+
+ IFile file = project.getFile("A.java");
+ file.create(new ByteArrayInputStream(contents), true, null);
+ if (sourceKind == SourceKind.FILE_CHARSET) {
+ file.setCharset(charset.name(), null);
+ } else if (sourceKind == SourceKind.PROJECT_CHARSET) {
+ project.setDefaultCharset(charset.name(), null);
+ }
+ return file;
+ }
+}
diff --git a/com.microsoft.java.debug.target/com.microsoft.java.debug.tp.target b/com.microsoft.java.debug.target/com.microsoft.java.debug.tp.target
index 31462a77..eeb70548 100644
--- a/com.microsoft.java.debug.target/com.microsoft.java.debug.tp.target
+++ b/com.microsoft.java.debug.target/com.microsoft.java.debug.tp.target
@@ -4,6 +4,7 @@
+