Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions src/java.base/share/classes/java/io/Console.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@
* on the objects returned by {@link #reader()} and {@link #writer()} may
* block in multithreaded scenarios.
* <p>
* Read and write operations use the {@code Charset} returned by the
* {@link #charset()} method, unless {@link System##stdin.encoding
* stdin.encoding} differs from {@link System##stdout.encoding
* stdout.encoding}, in which case read operations use the {@code Charset}
* designated by {@code stdin.encoding}.
* <p>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Console.charset() states "The returned charset is used for interpreting the input and output source (e.g., keyboard and/or display) specified by the host environment or user, which defaults to the one based on stdout.encoding." If stdin.encoding is set otherwise, this is no longer true, so I think this method may need a wording update as well.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Brought the same wording to the charset() method description for further clarification.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused by the actual behavior here. What might be helpful is to divide the discussion between a) what charsets get used for input and output, and b) the return value of the charset() method.

I'm not entirely sure, but since stdin.encoding and stdout.encoding are always set to something -- whether it comes from the platform or the command line -- won't Console just use stdin.encoding for input and stdout.encoding for output? If this is true, maybe just say this instead of deferring to the charset() method.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I agree with Stuart and it would be better to say that stdin.encoding is used for reading, and stdout.encoding for writing. They are usually the same but if they differ then Console will return the charset for output.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I reworded the descriptions of the class and charset() method.

* Operations that format strings are locale sensitive, using either the
* specified {@code Locale}, or the
* {@link Locale##default_locale default format Locale} to produce localized
Expand Down Expand Up @@ -549,7 +555,9 @@ private static UnsupportedOperationException newUnsupportedOperationException()
}

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);
private static final Console cons = instantiateConsole();
static {
Expand Down Expand Up @@ -579,7 +587,7 @@ private static Console instantiateConsole() {

for (var jcp : ServiceLoader.load(ModuleLayer.boot(), JdkConsoleProvider.class)) {
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) {
c = new ProxyingConsole(jc);
}
Expand All @@ -591,7 +599,7 @@ private static Console instantiateConsole() {

// If not found, default to built-in Console
if (istty && c == null) {
c = new ProxyingConsole(new JdkConsoleImpl(CHARSET));
c = new ProxyingConsole(new JdkConsoleImpl(STDIN_CHARSET, STDOUT_CHARSET));
}

return c;
Expand Down
17 changes: 10 additions & 7 deletions src/java.base/share/classes/jdk/internal/io/JdkConsoleImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -191,10 +191,11 @@ public void flush() {

@Override
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 writeLock;
// Must not block while holding this. It is used in the shutdown hook.
Expand Down Expand Up @@ -364,16 +365,18 @@ public int read(char[] cbuf, int offset, int length)
}
}

public JdkConsoleImpl(Charset charset) {
Objects.requireNonNull(charset);
this.charset = charset;
public JdkConsoleImpl(Charset inCharset, Charset outCharset) {
Objects.requireNonNull(inCharset);
Objects.requireNonNull(outCharset);
this.inCharset = inCharset;
this.outCharset = outCharset;
readLock = new Object();
writeLock = new Object();
restoreEchoLock = new Object();
out = StreamEncoder.forOutputStreamWriter(
new FileOutputStream(FileDescriptor.out),
writeLock,
charset);
outCharset);
pw = new PrintWriter(out, true) {
public void close() {
}
Expand All @@ -382,7 +385,7 @@ public void close() {
reader = new LineReader(StreamDecoder.forInputStreamReader(
new FileInputStream(FileDescriptor.in),
readLock,
charset));
inCharset));
rcb = new char[1024];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ public interface JdkConsoleProvider {
/**
* {@return the Console instance, or {@code null} if not available}
* @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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,18 @@ public class JdkConsoleProviderImpl implements JdkConsoleProvider {
* {@inheritDoc}
*/
@Override
public JdkConsole console(boolean isTTY, Charset charset) {
return new LazyDelegatingJdkConsoleImpl(charset);
public JdkConsole console(boolean isTTY, Charset inCharset, Charset outCharset) {
return new LazyDelegatingJdkConsoleImpl(inCharset, outCharset);
}

private static class LazyDelegatingJdkConsoleImpl implements JdkConsole {
private final Charset charset;
private final Charset outCharset;
private volatile boolean jlineInitialized;
private volatile JdkConsole delegate;

public LazyDelegatingJdkConsoleImpl(Charset charset) {
this.charset = charset;
this.delegate = new jdk.internal.io.JdkConsoleImpl(charset);
public LazyDelegatingJdkConsoleImpl(Charset inCharset, Charset outCharset) {
this.outCharset = outCharset;
this.delegate = new jdk.internal.io.JdkConsoleImpl(inCharset, outCharset);
}

@Override
Expand Down Expand Up @@ -130,7 +130,7 @@ public void flush() {

@Override
public Charset charset() {
return charset;
return outCharset;
}

private void flushOldDelegateIfNeeded(JdkConsole oldDelegate) {
Expand All @@ -157,7 +157,7 @@ private synchronized JdkConsole initializeJLineDelegate() {
}

try {
Terminal terminal = TerminalBuilder.builder().encoding(charset)
Terminal terminal = TerminalBuilder.builder().encoding(outCharset)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't ideally JdkConsole::charset and Terminal::encoding be adapted for stdin/stdout variants?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also noticed DumbTerminalProvider::sysTerminal calls DumbTerminal with new FileInputStream(FileDescriptor.in). Later on DumbTerminal applies encoding() both for passed stdin and std{out,err}. In short, TerminalProvider might need to undergo a similar refactoring separating input and output encodings.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All FileDescriptor.in encounters in jdk.internal.org.jline.terminal that might need attention:

src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/DumbTerminalProvider.java
src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/exec/ExecPty.java
src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/ffm/FfmTerminalProvider.java

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JLine is a 3rd party library. It would be desirable that they change their implementation to separately handle in/out in their terminal, but that is out of scope of this PR

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JLine seems to incorporate the stdin encoding already, which is quick!

.exec(false)
.nativeSignals(false)
.systemOutput(SystemOutput.SysOut)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public ConsoleProviderImpl() {
private static RemoteConsole console;

@Override
public JdkConsole console(boolean isTTY, Charset charset) {
public JdkConsole console(boolean isTTY, Charset inCharset, Charset outCharset) {
synchronized (ConsoleProviderImpl.class) {
if (remoteOutput != null && remoteInput != null) {
return console = new RemoteConsole(remoteOutput, remoteInput);
Expand Down
5 changes: 2 additions & 3 deletions test/jdk/java/io/Console/CharsetTest.java
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copyright year update is missing.

Original file line number Diff line number Diff line change
@@ -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.
*
* This code is free software; you can redistribute it and/or modify it
Expand Down Expand Up @@ -50,8 +50,7 @@ public static void main(String... args) throws Throwable {
// check "expect" command availability
var expect = Paths.get("/usr/bin/expect");
if (!Files.exists(expect) || !Files.isExecutable(expect)) {
System.out.println("'expect' command not found. Test ignored.");
return;
throw new jtreg.SkippedException("'expect' command not found. Test ignored.");
}

// invoking "expect" command
Expand Down
94 changes: 94 additions & 0 deletions test/jdk/java/io/Console/StdinEncodingTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* 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 jdk.test.lib.process.OutputAnalyzer;
import jdk.test.lib.process.ProcessTools;

import java.io.BufferedReader;
import java.nio.file.Files;
import java.nio.file.Paths;

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be helpful to include why the test is limited to only linux and mac,

"expect" command in Windows/Cygwin does not work as expected. Ignoring tests on Windows.

* the console.
* @requires (os.family == "linux" | os.family == "mac")
* @library /test/lib
* @build csp/*
* @run junit StdinEncodingTest
*/
public class StdinEncodingTest {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAICT, there is no similar test (e.g., one using a mock CharsetProvider) for stdout.encoding. Will it be addressed by another ticket? Shall we consider adding a similar StdoutEncodingTest too? (Not necessarily in this PR.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stdout.encoding validity is tested through the public charset() mehtod, which is in CharsetTest.java

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed there are stdout.encoding tests in test/jdk/java/io/Console, yet none1 that thoroughly tests them with expect and a dedicated (mock) CharsetProvider as you did here. FWIW, I really liked your new test using a mock CharsetProvider in combination with expect, hence my question for doing same for stdout and stderr too.

For the record, AFAICT, there are no tests for stderr.encoding.

1 There is script.exp, but it tests sun.stdout.encoding, not stdout.encoding.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually providing mock charset was a workaround of not having public method for getting the input encoding. I think it would be an overkill to introduce a new public method because it will not be used much, as most cases are suffice with the existing one (Console is used for interactive user enviornment, and I don't believe users would like to see different characters displayed for the input).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is script.exp, but it tests sun.stdout.encoding, not stdout.encoding.

It is addressed in this PR

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the JBS bug ID needs to be added to CharsetTest.java as well then.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bugid added to the test. Additionally replaced testsrc/jdk/classes with Utils ones


@Test
public void testStdinEncoding() throws Throwable {
// check "expect" command availability
var expect = Paths.get("/usr/bin/expect");
if (!Files.exists(expect) || !Files.isExecutable(expect)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could use Assumptions.assumeTrue here. Condition becomes more readable as: Files.exists(expect) && Files.isExecutable(expect)

Assumptions.abort("'" + expect + "' not found");
}

// invoking "expect" command
var testSrc = System.getProperty("test.src", ".");
Copy link
Member

@justin-curtis-lu justin-curtis-lu May 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test already imports the JDK test lib, can we just replace this and the below ocurrences with jdk.test.lib.Utils.TEST_SRC/JDK/CLASSES directly?

var testClasses = System.getProperty("test.classes", ".");
var jdkDir = System.getProperty("test.jdk");
OutputAnalyzer output = ProcessTools.executeProcess(
"expect",
"-n",
testSrc + "/stdinEncoding.exp",
jdkDir + "/bin/java",
"--module-path",
testClasses + "/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();
}
}
26 changes: 26 additions & 0 deletions test/jdk/java/io/Console/csp/module-info.java
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
4 changes: 2 additions & 2 deletions test/jdk/java/io/Console/script.exp
Original file line number Diff line number Diff line change
@@ -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.
#
# This code is free software; you can redistribute it and/or modify it
Expand Down Expand Up @@ -27,6 +27,6 @@ set expected [lrange $argv 2 2]
set args [lrange $argv 3 end]
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 eof
Loading