From 5a6483183afaff90e2711b1df3fef29e6f4f2f0a Mon Sep 17 00:00:00 2001 From: chagong <831821+chagong@users.noreply.github.com> Date: Thu, 29 Oct 2026 02:13:03 +0000 Subject: [PATCH 1/2] fix: honor source encodings when resolving breakpoints Use Eclipse resource decoding for workspace files and BOM-aware decoding for external sources. Exercise breakpoint class-name resolution with UTF-16, UTF-8, and configured resource charsets in headless JDT tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 9 ++ com.microsoft.java.debug.plugin/pom.xml | 54 +++++++ .../internal/JdtSourceLookUpProvider.java | 39 ++--- .../internal/JdtSourceLookUpProviderTest.java | 142 ++++++++++++++++++ .../com.microsoft.java.debug.tp.target | 1 + 5 files changed, 226 insertions(+), 19 deletions(-) create mode 100644 com.microsoft.java.debug.plugin/src/test/java/com/microsoft/java/debug/plugin/internal/JdtSourceLookUpProviderTest.java diff --git a/README.md b/README.md index 4bf15f675..12a63cc69 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,12 @@ The Java Debug Server is an implementation of Visual Studio Code (VSCode) Debug - Threads - Debug console +### Source encodings + +File-backed source resolution, including line breakpoints, uses the workspace resource's +encoding when available. External source files recognize UTF-8 and UTF-16 LE/BE byte +order marks; without a byte order mark, they use the language-server JVM's default charset. + ## Background The Java Debug Server is the bridge between VSCode and JVM. The implementation is based on JDI ([Java Debug Interface](https://docs.oracle.com/javase/7/docs/jdk/api/jpda/jdi/)). It works with [Eclipse JDT Language Server](https://github.com/vscjavaci/eclipse.jdt.ls) as an add-on to provide debug functionalities. @@ -26,6 +32,9 @@ The Java Debug Server is the bridge between VSCode and JVM. The implementation i ## Installation +Builds require JDK 21 or newer. `./mvnw clean verify` runs the core tests and +the headless Eclipse/JDT plugin regression tests. + ### Windows: ``` mvnw.cmd clean install diff --git a/com.microsoft.java.debug.plugin/pom.xml b/com.microsoft.java.debug.plugin/pom.xml index 2e2c0e6a8..400c68829 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 2c39425ed..75743365c 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 000000000..37fc42e4b --- /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 31462a770..eeb70548e 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 @@ + From 46db436a48270e53eb169f868d19ffdef2ed6d0b Mon Sep 17 00:00:00 2001 From: chagong <831821+chagong@users.noreply.github.com> Date: Thu, 29 Oct 2026 02:29:02 +0000 Subject: [PATCH 2/2] docs: keep encoding fix notes out of public README Restore README.md to the PR base without changing the source-encoding fix or regression tests. Keep contributor-facing verification and implementation rationale in the PR description because there is no suitable existing contributor guide. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/README.md b/README.md index 12a63cc69..4bf15f675 100644 --- a/README.md +++ b/README.md @@ -15,12 +15,6 @@ The Java Debug Server is an implementation of Visual Studio Code (VSCode) Debug - Threads - Debug console -### Source encodings - -File-backed source resolution, including line breakpoints, uses the workspace resource's -encoding when available. External source files recognize UTF-8 and UTF-16 LE/BE byte -order marks; without a byte order mark, they use the language-server JVM's default charset. - ## Background The Java Debug Server is the bridge between VSCode and JVM. The implementation is based on JDI ([Java Debug Interface](https://docs.oracle.com/javase/7/docs/jdk/api/jpda/jdi/)). It works with [Eclipse JDT Language Server](https://github.com/vscjavaci/eclipse.jdt.ls) as an add-on to provide debug functionalities. @@ -32,9 +26,6 @@ The Java Debug Server is the bridge between VSCode and JVM. The implementation i ## Installation -Builds require JDK 21 or newer. `./mvnw clean verify` runs the core tests and -the headless Eclipse/JDT plugin regression tests. - ### Windows: ``` mvnw.cmd clean install