8356985: Use "stdin.encoding" in Console's read*() methods

Reviewed-by: jlu, smarks, alanb, vyazici
This commit is contained in:
Naoto Sato 2025-05-28 16:24:04 +00:00
parent 8949c07484
commit b2a61a9972
11 changed files with 317 additions and 41 deletions

View File

@ -59,6 +59,13 @@ import sun.nio.cs.UTF_8;
* on the objects returned by {@link #reader()} and {@link #writer()} may * on the objects returned by {@link #reader()} and {@link #writer()} may
* block in multithreaded scenarios. * block in multithreaded scenarios.
* <p> * <p>
* Read and write operations use the {@code Charset}s specified by
* {@link System##stdin.encoding stdin.encoding} and {@link
* System##stdout.encoding stdout.encoding}, respectively. The
* {@code Charset} used for write operations can also be retrieved using
* the {@link #charset()} method. Since {@code Console} is intended for
* interactive use on a terminal, these charsets are typically the same.
* <p>
* Operations that format strings are locale sensitive, using either the * Operations that format strings are locale sensitive, using either the
* specified {@code Locale}, or the * specified {@code Locale}, or the
* {@link Locale##default_locale default format Locale} to produce localized * {@link Locale##default_locale default format Locale} to produce localized
@ -509,17 +516,15 @@ public sealed class Console implements Flushable permits ProxyingConsole {
} }
/** /**
* Returns the {@link java.nio.charset.Charset Charset} object used for * {@return the {@link java.nio.charset.Charset Charset} object used for
* the {@code Console}. * the write operations on this {@code Console}}
* <p> * <p>
* The returned charset is used for interpreting the input and output source * The returned charset is used for encoding the data that is sent to
* (e.g., keyboard and/or display) specified by the host environment or user, * the output (e.g., display), specified by the host environment or user.
* which defaults to the one based on {@link System##stdout.encoding stdout.encoding}. * It defaults to the one based on {@link System##stdout.encoding stdout.encoding},
* It may not necessarily be the same as the default charset returned from * and may not necessarily be the same as the default charset returned from
* {@link java.nio.charset.Charset#defaultCharset() Charset.defaultCharset()}. * {@link java.nio.charset.Charset#defaultCharset() Charset.defaultCharset()}.
* *
* @return a {@link java.nio.charset.Charset Charset} object used for the
* {@code Console}
* @since 17 * @since 17
*/ */
public Charset charset() { public Charset charset() {
@ -549,7 +554,9 @@ public sealed class Console implements Flushable permits ProxyingConsole {
} }
private static final boolean istty = istty(); private static final boolean istty = istty();
static final Charset CHARSET = private static final Charset STDIN_CHARSET =
Charset.forName(System.getProperty("stdin.encoding"), UTF_8.INSTANCE);
private static final Charset STDOUT_CHARSET =
Charset.forName(System.getProperty("stdout.encoding"), UTF_8.INSTANCE); Charset.forName(System.getProperty("stdout.encoding"), UTF_8.INSTANCE);
private static final Console cons = instantiateConsole(); private static final Console cons = instantiateConsole();
static { static {
@ -579,7 +586,7 @@ public sealed class Console implements Flushable permits ProxyingConsole {
for (var jcp : ServiceLoader.load(ModuleLayer.boot(), JdkConsoleProvider.class)) { for (var jcp : ServiceLoader.load(ModuleLayer.boot(), JdkConsoleProvider.class)) {
if (consModName.equals(jcp.getClass().getModule().getName())) { if (consModName.equals(jcp.getClass().getModule().getName())) {
var jc = jcp.console(istty, CHARSET); var jc = jcp.console(istty, STDIN_CHARSET, STDOUT_CHARSET);
if (jc != null) { if (jc != null) {
c = new ProxyingConsole(jc); c = new ProxyingConsole(jc);
} }
@ -591,7 +598,7 @@ public sealed class Console implements Flushable permits ProxyingConsole {
// If not found, default to built-in Console // If not found, default to built-in Console
if (istty && c == null) { if (istty && c == null) {
c = new ProxyingConsole(new JdkConsoleImpl(CHARSET)); c = new ProxyingConsole(new JdkConsoleImpl(STDIN_CHARSET, STDOUT_CHARSET));
} }
return c; return c;

View File

@ -191,10 +191,11 @@ public final class JdkConsoleImpl implements JdkConsole {
@Override @Override
public Charset charset() { public Charset charset() {
return charset; return outCharset;
} }
private final Charset charset; private final Charset inCharset;
private final Charset outCharset;
private final Object readLock; private final Object readLock;
private final Object writeLock; private final Object writeLock;
// Must not block while holding this. It is used in the shutdown hook. // Must not block while holding this. It is used in the shutdown hook.
@ -364,16 +365,18 @@ public final class JdkConsoleImpl implements JdkConsole {
} }
} }
public JdkConsoleImpl(Charset charset) { public JdkConsoleImpl(Charset inCharset, Charset outCharset) {
Objects.requireNonNull(charset); Objects.requireNonNull(inCharset);
this.charset = charset; Objects.requireNonNull(outCharset);
this.inCharset = inCharset;
this.outCharset = outCharset;
readLock = new Object(); readLock = new Object();
writeLock = new Object(); writeLock = new Object();
restoreEchoLock = new Object(); restoreEchoLock = new Object();
out = StreamEncoder.forOutputStreamWriter( out = StreamEncoder.forOutputStreamWriter(
new FileOutputStream(FileDescriptor.out), new FileOutputStream(FileDescriptor.out),
writeLock, writeLock,
charset); outCharset);
pw = new PrintWriter(out, true) { pw = new PrintWriter(out, true) {
public void close() { public void close() {
} }
@ -382,7 +385,7 @@ public final class JdkConsoleImpl implements JdkConsole {
reader = new LineReader(StreamDecoder.forInputStreamReader( reader = new LineReader(StreamDecoder.forInputStreamReader(
new FileInputStream(FileDescriptor.in), new FileInputStream(FileDescriptor.in),
readLock, readLock,
charset)); inCharset));
rcb = new char[1024]; rcb = new char[1024];
} }
} }

View File

@ -38,7 +38,8 @@ public interface JdkConsoleProvider {
/** /**
* {@return the Console instance, or {@code null} if not available} * {@return the Console instance, or {@code null} if not available}
* @param isTTY indicates if the jvm is attached to a terminal * @param isTTY indicates if the jvm is attached to a terminal
* @param charset charset of the platform console * @param inCharset Standard input charset of the platform console
* @param outCharset Standard output charset of the platform console
*/ */
JdkConsole console(boolean isTTY, Charset charset); JdkConsole console(boolean isTTY, Charset inCharset, Charset outCharset);
} }

View File

@ -49,18 +49,18 @@ public class JdkConsoleProviderImpl implements JdkConsoleProvider {
* {@inheritDoc} * {@inheritDoc}
*/ */
@Override @Override
public JdkConsole console(boolean isTTY, Charset charset) { public JdkConsole console(boolean isTTY, Charset inCharset, Charset outCharset) {
return new LazyDelegatingJdkConsoleImpl(charset); return new LazyDelegatingJdkConsoleImpl(inCharset, outCharset);
} }
private static class LazyDelegatingJdkConsoleImpl implements JdkConsole { private static class LazyDelegatingJdkConsoleImpl implements JdkConsole {
private final Charset charset; private final Charset outCharset;
private volatile boolean jlineInitialized; private volatile boolean jlineInitialized;
private volatile JdkConsole delegate; private volatile JdkConsole delegate;
public LazyDelegatingJdkConsoleImpl(Charset charset) { public LazyDelegatingJdkConsoleImpl(Charset inCharset, Charset outCharset) {
this.charset = charset; this.outCharset = outCharset;
this.delegate = new jdk.internal.io.JdkConsoleImpl(charset); this.delegate = new jdk.internal.io.JdkConsoleImpl(inCharset, outCharset);
} }
@Override @Override
@ -130,7 +130,7 @@ public class JdkConsoleProviderImpl implements JdkConsoleProvider {
@Override @Override
public Charset charset() { public Charset charset() {
return charset; return outCharset;
} }
private void flushOldDelegateIfNeeded(JdkConsole oldDelegate) { private void flushOldDelegateIfNeeded(JdkConsole oldDelegate) {
@ -157,7 +157,7 @@ public class JdkConsoleProviderImpl implements JdkConsoleProvider {
} }
try { try {
Terminal terminal = TerminalBuilder.builder().encoding(charset) Terminal terminal = TerminalBuilder.builder().encoding(outCharset)
.exec(false) .exec(false)
.nativeSignals(false) .nativeSignals(false)
.systemOutput(SystemOutput.SysOut) .systemOutput(SystemOutput.SysOut)

View File

@ -64,7 +64,7 @@ public class ConsoleImpl {
private static RemoteConsole console; private static RemoteConsole console;
@Override @Override
public JdkConsole console(boolean isTTY, Charset charset) { public JdkConsole console(boolean isTTY, Charset inCharset, Charset outCharset) {
synchronized (ConsoleProviderImpl.class) { synchronized (ConsoleProviderImpl.class) {
if (remoteOutput != null && remoteInput != null) { if (remoteOutput != null && remoteInput != null) {
return console = new RemoteConsole(remoteOutput, remoteInput); return console = new RemoteConsole(remoteOutput, remoteInput);

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* *
* This code is free software; you can redistribute it and/or modify it * This code is free software; you can redistribute it and/or modify it
@ -27,10 +27,11 @@ import java.nio.file.Paths;
import jdk.test.lib.process.OutputAnalyzer; import jdk.test.lib.process.OutputAnalyzer;
import jdk.test.lib.process.ProcessTools; import jdk.test.lib.process.ProcessTools;
import static jdk.test.lib.Utils.*;
/** /**
* @test * @test
* @bug 8264208 8265918 * @bug 8264208 8265918 8356985
* @summary Tests Console.charset() method. "expect" command in Windows/Cygwin * @summary Tests Console.charset() method. "expect" command in Windows/Cygwin
* does not work as expected. Ignoring tests on Windows. * does not work as expected. Ignoring tests on Windows.
* @requires (os.family == "linux") | (os.family == "mac") * @requires (os.family == "linux") | (os.family == "mac")
@ -50,22 +51,18 @@ public class CharsetTest {
// check "expect" command availability // check "expect" command availability
var expect = Paths.get("/usr/bin/expect"); var expect = Paths.get("/usr/bin/expect");
if (!Files.exists(expect) || !Files.isExecutable(expect)) { if (!Files.exists(expect) || !Files.isExecutable(expect)) {
System.out.println("'expect' command not found. Test ignored."); throw new jtreg.SkippedException("'expect' command not found. Test ignored.");
return;
} }
// invoking "expect" command // invoking "expect" command
var testSrc = System.getProperty("test.src", ".");
var testClasses = System.getProperty("test.classes", ".");
var jdkDir = System.getProperty("test.jdk");
OutputAnalyzer output = ProcessTools.executeProcess( OutputAnalyzer output = ProcessTools.executeProcess(
"expect", "expect",
"-n", "-n",
testSrc + "/script.exp", TEST_SRC + "/script.exp",
jdkDir + "/bin/java", TEST_JDK + "/bin/java",
args[0], args[0],
args[1], args[1],
testClasses); TEST_CLASSES);
output.reportDiagnosticSummary(); output.reportDiagnosticSummary();
var eval = output.getExitValue(); var eval = output.getExitValue();
if (eval != 0) { if (eval != 0) {

View File

@ -0,0 +1,92 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is 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
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import java.io.BufferedReader;
import java.nio.file.Files;
import java.nio.file.Paths;
import jdk.test.lib.process.OutputAnalyzer;
import jdk.test.lib.process.ProcessTools;
import static jdk.test.lib.Utils.*;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* @test
* @bug 8356985
* @summary Tests if "stdin.encoding" is reflected for reading
* the console. "expect" command in Windows/Cygwin does
* not work as expected. Ignoring tests on Windows.
* @requires (os.family == "linux" | os.family == "mac")
* @library /test/lib
* @build csp/*
* @run junit StdinEncodingTest
*/
public class StdinEncodingTest {
@Test
public void testStdinEncoding() throws Throwable {
// check "expect" command availability
var expect = Paths.get("/usr/bin/expect");
Assumptions.assumeTrue(Files.exists(expect) && Files.isExecutable(expect),
"'" + expect + "' not found");
// invoking "expect" command
OutputAnalyzer output = ProcessTools.executeProcess(
"expect",
"-n",
TEST_SRC + "/stdinEncoding.exp",
TEST_JDK + "/bin/java",
"--module-path",
TEST_CLASSES + "/modules",
"-Dstdin.encoding=Uppercasing", // <- gist of this test
"StdinEncodingTest");
output.reportDiagnosticSummary();
var eval = output.getExitValue();
assertEquals(0, eval, "Test failed. Exit value from 'expect' command: " + eval);
}
public static void main(String... args) throws Throwable {
// check stdin.encoding
if (!"Uppercasing".equals(System.getProperty("stdin.encoding"))) {
throw new RuntimeException("Uppercasing charset was not set in stdin.encoding");
}
var con = System.console();
// Console.readLine()
System.out.print(con.readLine());
// Console.readPassword()
System.out.print(String.valueOf(con.readPassword()));
// Console.reader()
try (var br = new BufferedReader(con.reader())) {
System.out.print(br.readLine());
}
// Wait till the test receives the result
con.readLine();
}
}

View File

@ -0,0 +1,26 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is 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
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
module csp {
provides java.nio.charset.spi.CharsetProvider with provider.UppercasingCharsetProvider;
}

View File

@ -0,0 +1,87 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is 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
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package provider;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CoderResult;
import java.nio.charset.spi.CharsetProvider;
import java.util.Collections;
import java.util.Iterator;
// A test charset provider that decodes every input byte into its uppercase
public class UppercasingCharsetProvider extends CharsetProvider {
@Override
public Iterator charsets() {
return Collections.singleton(new UppercasingCharsetProvider.UppercasingCharset()).iterator();
}
@Override
public Charset charsetForName(String charsetName) {
if (charsetName.equals("Uppercasing")) {
return new UppercasingCharsetProvider.UppercasingCharset();
} else {
return null;
}
}
public static class UppercasingCharset extends Charset {
public UppercasingCharset() {
super("Uppercasing", null);
}
@Override
public boolean contains(Charset cs) {
return false;
}
@Override
public CharsetDecoder newDecoder() {
return new UppercasingCharsetDecoder(this, 1, 1);
}
@Override
public CharsetEncoder newEncoder() {
return null;
}
}
private static class UppercasingCharsetDecoder extends CharsetDecoder {
public UppercasingCharsetDecoder(Charset cs, float averageCharsPerByte, float maxCharsPerByte) {
super(cs, averageCharsPerByte, maxCharsPerByte);
}
@Override
protected CoderResult decodeLoop(ByteBuffer in, CharBuffer out) {
while (in.remaining() > 0) {
out.put(Character.toUpperCase((char)in.get()));
}
return CoderResult.UNDERFLOW;
}
}
}

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved. # Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved.
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
# #
# This code is free software; you can redistribute it and/or modify it # This code is free software; you can redistribute it and/or modify it
@ -27,6 +27,6 @@ set expected [lrange $argv 2 2]
set args [lrange $argv 3 end] set args [lrange $argv 3 end]
regexp {([a-zA-Z_]*).([a-zA-Z0-9\-]*)} $locale dummy lang_region encoding regexp {([a-zA-Z_]*).([a-zA-Z0-9\-]*)} $locale dummy lang_region encoding
eval spawn $java -Dsun.stdout.encoding=$encoding -classpath $args CharsetTest eval spawn $java -Dstdout.encoding=$encoding -classpath $args CharsetTest
expect $expected expect $expected
expect eof expect eof

View File

@ -0,0 +1,63 @@
#
# Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
#
# This code is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 2 only, as
# published by the Free Software Foundation.
#
# This code is 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
# version 2 for more details (a copy is included in the LICENSE file that
# accompanied this code).
#
# You should have received a copy of the GNU General Public License version
# 2 along with this work; if not, write to the Free Software Foundation,
# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
# or visit www.oracle.com if you need additional information or have any
# questions.
#
set java [lrange $argv 0 end]
eval spawn $java
# Console::readLine()
send "abc\r"
expect {
"ABC" { send_error "Console::readLine() received\n" }
timeout {
send_error "Error: Console::readLine() not received\n"
exit 1
}
}
# Console::readPassword()
send "def\r"
expect {
"DEF" { send_error "Console::readPassword() received\n" }
timeout {
send_error "Error: Console::readPassword() not received\n"
exit 1
}
}
# Console::reader()
send "ghi\r"
expect {
"GHI" { send_error "Console::reader() received\n" }
timeout {
send_error "Error: Console::reader() not received\n"
exit 1
}
}
# should receive eof
send "\r"
expect eof
# success
exit 0