1 /*
  2  * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.
  8  *
  9  * This code is distributed in the hope that it will be useful, but WITHOUT
 10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 12  * version 2 for more details (a copy is included in the LICENSE file that
 13  * accompanied this code).
 14  *
 15  * You should have received a copy of the GNU General Public License version
 16  * 2 along with this work; if not, write to the Free Software Foundation,
 17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 18  *
 19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 20  * or visit www.oracle.com if you need additional information or have any
 21  * questions.
 22  */
 23 
 24 import static org.testng.Assert.*;
 25 
 26 import org.testng.Assert;
 27 import org.testng.annotations.DataProvider;
 28 import org.testng.annotations.Test;
 29 
 30 import jdk.test.lib.process.ProcessTools;
 31 
 32 import jdk.test.lib.hexdump.HexPrinter;
 33 import jdk.test.lib.hexdump.HexPrinter.Formatters;
 34 
 35 import java.io.BufferedReader;
 36 import java.io.BufferedWriter;
 37 import java.io.IOException;
 38 import java.io.Writer;
 39 import java.nio.ByteBuffer;
 40 import java.nio.file.Path;
 41 import java.nio.file.Files;
 42 import java.nio.charset.Charset;
 43 import java.nio.charset.StandardCharsets;
 44 import java.nio.charset.UnsupportedCharsetException;
 45 import java.util.List;
 46 import java.util.Locale;
 47 
 48 import jtreg.SkippedException;
 49 
 50 /*
 51  * @test
 52  * @library /test/lib
 53  * @build jdk.test.lib.process.ProcessTools jdk.test.lib.hexdump.HexPrinter
 54  * @run testng ReaderWriterTest
 55  */
 56 
 57 @Test
 58 public class ReaderWriterTest {
 59 
 60     static final String ASCII = "ASCII: \u0000_A-Z_a-Z_\u007C_\u007D_\u007E_\u007F_;";
 61     static final String ISO_8859_1 = " Symbols: \u00AB_\u00BB_\u00fc_\u00fd_\u00fe_\u00ff;";
 62     static final String FRACTIONS = " Fractions: \u00bc_\u00bd_\u00be_\u00bf;";
 63 
 64     public static final String TESTCHARS = "OneWay: " + ASCII + ISO_8859_1 + FRACTIONS;
 65     public static final String ROUND_TRIP_TESTCHARS = "RoundTrip: " + ASCII + ISO_8859_1 + FRACTIONS;
 66 
 67     @DataProvider(name="CharsetCases")
 68     static Object[][] charsetCases() {
 69         return new Object[][] {
 70                 {"UTF-8"},
 71                 {"ISO8859-1"},
 72                 {"US-ASCII"},
 73         };
 74     }
 75 
 76     /**
 77      * Test the defaults case of native.encoding.  No extra command line flags or switches.
 78      */
 79     @Test
 80     void testCaseNativeEncoding() throws IOException {
 81         String nativeEncoding = System.getProperty("native.encoding");
 82         Charset cs = Charset.forName(nativeEncoding);
 83         System.out.println("Native.encoding Charset: " + cs);
 84 
 85         ProcessBuilder pb = ProcessTools.createLimitedTestJavaProcessBuilder("ReaderWriterTest$ChildWithCharset");
 86         Process p = pb.start();
 87         writeTestChars(p.outputWriter());
 88         checkReader(p.inputReader(), cs, "Out");
 89         checkReader(p.errorReader(), cs, "Err");
 90         try {
 91             int exitValue = p.waitFor();
 92             if (exitValue != 0)
 93                 System.out.println("exitValue: " + exitValue);
 94         } catch (InterruptedException ie) {
 95             Assert.fail("waitFor interrupted");
 96         }
 97     }
 98 
 99     /**
100      * Test that redirects of input and error streams result in Readers that are empty.
101      * Test that when the output to a process is redirected, the writer acts as
102      * a null stream and throws an exception as expected for a null output stream
103      * as specified by ProcessBuilder.
104      */
105     @Test
106     void testRedirects() throws IOException {
107         String nativeEncoding = System.getProperty("native.encoding");
108         Charset cs = Charset.forName(nativeEncoding);
109         System.out.println("Native.encoding Charset: " + cs);
110 
111         Path inPath = Path.of("InFile.tmp");
112         BufferedWriter inWriter = Files.newBufferedWriter(inPath);
113         inWriter.close();
114 
115         Path outPath = Path.of("OutFile.tmp");
116         Path errorPath = Path.of("ErrFile.tmp");
117 
118         for (int errType = 1; errType < 4; errType++) {
119             // Three cases to test for which the error stream is empty
120             // 1: redirectErrorStream(false); redirect of errorOutput to a file
121             // 2: redirectErrorStream(true); no redirect of errorOutput
122             // 3: redirectErrorStream(true); redirect of errorOutput to a file
123 
124             ProcessBuilder pb = ProcessTools.createLimitedTestJavaProcessBuilder("ReaderWriterTest$ChildWithCharset");
125             pb.redirectInput(inPath.toFile());
126             pb.redirectOutput(outPath.toFile());
127             if (errType == 1 || errType == 3) {
128                 pb.redirectError(errorPath.toFile());
129             }
130             if (errType == 2 || errType == 3) {
131                 pb.redirectErrorStream(true);
132             }
133             Process p = pb.start();
134             // Output has been redirected to a null stream; success is IOException on the write
135             try {
136                 BufferedWriter wr = p.outputWriter();
137                 wr.write("X");
138                 wr.flush();
139                 Assert.fail("writing to null stream should throw IOException");
140             } catch (IOException ioe) {
141                 // Normal, A Null output stream is closed when created.
142             }
143 
144             // InputReader should be empty; and at EOF
145             BufferedReader inputReader = p.inputReader();
146             int ch = inputReader.read();
147             Assert.assertEquals(ch, -1, "inputReader not at EOF: ch: " + (char)ch);
148 
149             // InputReader should be empty; and at EOF
150             BufferedReader errorReader = p.errorReader();
151             ch = errorReader.read();
152             Assert.assertEquals(ch, -1, "errorReader not at EOF: ch: " + (char)ch);
153 
154             try {
155                 int exitValue = p.waitFor();
156                 if (exitValue != 0) System.out.println("exitValue: " + exitValue);
157             } catch (InterruptedException ie) {
158                 Assert.fail("waitFor interrupted");
159             }
160         }
161     }
162 
163     /**
164      * Write the test characters to the child using the Process.outputWriter.
165      * @param writer the Writer
166      * @throws IOException if an I/O error occurs
167      */
168     private static void writeTestChars(Writer writer) throws IOException {
169         // Write the test data to the child
170         try (writer) {
171             writer.append(ROUND_TRIP_TESTCHARS);
172             writer.append(System.lineSeparator());
173         }
174     }
175 
176     /**
177      * Test a child with a character set.
178      * A Process is spawned; characters are written to and read from the child
179      * using the character set and compared.
180      *
181      * @param encoding a charset name
182      */
183     @Test(dataProvider = "CharsetCases", enabled = true)
184     void testCase(String encoding) throws IOException {
185         Charset cs = null;
186         try {
187             cs = Charset.forName(encoding);
188             System.out.println("Charset: " + cs);
189         } catch (UnsupportedCharsetException use) {
190             throw new SkippedException("Charset not supported: " + encoding);
191         }
192         String cleanCSName = cleanCharsetName(cs);
193 
194         ProcessBuilder pb = ProcessTools.createLimitedTestJavaProcessBuilder(
195                 "-Dsun.stdout.encoding=" + cleanCSName,     // Encode in the child using the charset
196                 "-Dsun.stderr.encoding=" + cleanCSName,
197                 "ReaderWriterTest$ChildWithCharset");
198 
199         Process p = pb.start();
200         // Write the test data to the child
201         writeTestChars(p.outputWriter(cs));
202         checkReader(p.inputReader(cs), cs, "Out");
203         checkReader(p.errorReader(cs), cs, "Err");
204         try {
205             int exitValue = p.waitFor();
206             if (exitValue != 0)
207                 System.out.println("exitValue: " + exitValue);
208         } catch (InterruptedException ie) {
209 
210         }
211     }
212 
213     /**
214      * Test passing null when a charset is expected
215      * @throws IOException if an I/O error occurs; not expected
216      */
217     @Test
218     void testNullCharsets()  throws IOException {
219         // Launch a child; its behavior is not interesting and is ignored
220         ProcessBuilder pb = ProcessTools.createLimitedTestJavaProcessBuilder(
221                 "ReaderWriterTest$ChildWithCharset");
222 
223         Process p = pb.start();
224         try {
225             writeTestChars(p.outputWriter(null));
226             Assert.fail("Process.outputWriter(null) did not throw NPE");
227         } catch (NullPointerException npe) {
228             // expected, ignore
229         }
230         try {
231             checkReader(p.inputReader(null), null, "Out");
232             Assert.fail("Process.inputReader(null) did not throw NPE");
233         } catch (NullPointerException npe) {
234             // expected, ignore
235         }
236         try {
237             checkReader(p.errorReader(null), null, "Err");
238             Assert.fail("Process.errorReader(null) did not throw NPE");
239         } catch (NullPointerException npe) {
240             // expected, ignore
241         }
242 
243         p.destroyForcibly();
244         try {
245             // Collect the exit status to cleanup after the process; but ignore it
246             p.waitFor();
247         } catch (InterruptedException ie) {
248             // Ignored
249         }
250     }
251 
252     /**
253      * Test passing different charset on multiple calls when the same charset is expected.
254      * @throws IOException if an I/O error occurs; not expected
255      */
256     @Test
257     void testIllegalArgCharsets()  throws IOException {
258         String nativeEncoding = System.getProperty("native.encoding");
259         Charset cs = Charset.forName(nativeEncoding);
260         System.out.println("Native.encoding Charset: " + cs);
261         Charset otherCharset = cs.equals(StandardCharsets.UTF_8)
262                 ? StandardCharsets.ISO_8859_1
263                 : StandardCharsets.UTF_8;
264 
265         // Launch a child; its behavior is not interesting and is ignored
266         ProcessBuilder pb = ProcessTools.createLimitedTestJavaProcessBuilder(
267                 "ReaderWriterTest$ChildWithCharset");
268 
269         Process p = pb.start();
270         try {
271             var writer = p.outputWriter(cs);
272             writer = p.outputWriter(cs);        // try again with same
273             writer = p.outputWriter(otherCharset);  // this should throw
274             Assert.fail("Process.outputWriter(otherCharset) did not throw IllegalStateException");
275         } catch (IllegalStateException ile) {
276             // expected, ignore
277             System.out.println(ile);
278         }
279         try {
280             var reader = p.inputReader(cs);
281             reader = p.inputReader(cs);             // try again with same
282             reader = p.inputReader(otherCharset);   // this should throw
283             Assert.fail("Process.inputReader(otherCharset) did not throw IllegalStateException");
284         } catch (IllegalStateException ile) {
285             // expected, ignore
286             System.out.println(ile);
287         }
288         try {
289             var reader = p.errorReader(cs);
290             reader = p.errorReader(cs);             // try again with same
291             reader = p.errorReader(otherCharset);   // this should throw
292             Assert.fail("Process.errorReader(otherCharset) did not throw IllegalStateException");
293         } catch (IllegalStateException ile) {
294             // expected, ignore
295             System.out.println(ile);
296         }
297 
298         p.destroyForcibly();
299         try {
300             // Collect the exit status to cleanup after the process; but ignore it
301             p.waitFor();
302         } catch (InterruptedException ie) {
303             // Ignored
304         }
305     }
306 
307     private static void checkReader(BufferedReader reader, Charset cs, String label) throws IOException {
308         try (BufferedReader in = reader) {
309             String prefix = "    " + label + ": ";
310             String firstline = in.readLine();
311             System.out.append(prefix).println(firstline);
312             String secondline = in.readLine();
313             System.out.append(prefix).println(secondline);
314             for (String line = in.readLine(); line != null; line = in.readLine()) {
315                 System.out.append(prefix).append(line);
316                 System.out.println();
317             }
318             ByteBuffer bb = cs.encode(TESTCHARS);
319             String reencoded = cs.decode(bb).toString();
320             if (!firstline.equals(reencoded))
321                 diffStrings(firstline, reencoded);
322             assertEquals(firstline, reencoded, label + " Test Chars");
323 
324             bb = cs.encode(ROUND_TRIP_TESTCHARS);
325             reencoded = cs.decode(bb).toString();
326             if (!secondline.equals(reencoded))
327                 diffStrings(secondline, reencoded);
328             assertEquals(secondline, reencoded, label + " Round Trip Test Chars");
329         }
330     }
331 
332     /**
333      * A cleaned up Charset name that is suitable for Linux LANG environment variable.
334      * If there are two '-'s the first one is removed.
335      * @param cs a Charset
336      * @return the cleanedup Charset name
337      */
338     private static String cleanCharsetName(Charset cs) {
339         String name = cs.name();
340         int ndx = name.indexOf('-');
341         if (ndx >= 0 && name.indexOf('-', ndx + 1) >= 0) {
342             name = name.substring(0, ndx) + name.substring(ndx + 1);
343         }
344         return name;
345     }
346 
347     private static void diffStrings(String actual, String expected) {
348         if (actual.equals(expected))
349             return;
350         int lenDiff = expected.length() - actual.length();
351         if (lenDiff != 0)
352             System.out.println("String lengths:  " + actual.length() + " != " + expected.length());
353         int first;  // find first mismatched character
354         for (first = 0; first < Math.min(actual.length(), expected.length()); first++) {
355             if (actual.charAt(first) != expected.charAt(first))
356                 break;
357         }
358         int last;
359         for (last = actual.length() - 1; last >= 0 && (last + lenDiff) >= 0; last--) {
360             if (actual.charAt(last) != expected.charAt(last + lenDiff))
361                 break;      // last mismatched character
362         }
363         System.out.printf("actual vs expected[%3d]: 0x%04x != 0x%04x%n", first, (int)actual.charAt(first), (int)expected.charAt(first));
364         System.out.printf("actual vs expected[%3d]: 0x%04x != 0x%04x%n", last, (int)actual.charAt(last), (int)expected.charAt(last));
365         System.out.printf("actual  [%3d-%3d]: %s%n", first, last, actual.substring(first, last+1));
366         System.out.printf("expected[%3d-%3d]: %s%n", first, last, expected.substring(first, last + lenDiff + 1));
367     }
368 
369     static class ChildWithCharset {
370         public static void main(String[] args) {
371             String nativeEncoding = System.getProperty("native.encoding");
372             System.out.println(TESTCHARS);
373             byte[] bytes = null;
374             try {
375                 bytes = System.in.readAllBytes();
376                 System.out.write(bytes);    // echo bytes back to parent on stdout
377             } catch (IOException ioe) {
378                 ioe.printStackTrace();      // Seen by the parent
379             }
380             System.out.println("native.encoding: " + nativeEncoding);
381             System.out.println("sun.stdout.encoding: " + System.getProperty("sun.stdout.encoding"));
382             System.out.println("LANG: " + System.getenv().get("LANG"));
383 
384             System.err.println(TESTCHARS);
385             try {
386                 System.err.write(bytes);    // echo bytes back to parent on stderr
387             } catch (IOException ioe) {
388                 ioe.printStackTrace();      // Seen by the parent
389             }
390             System.err.println("native.encoding: " + nativeEncoding);
391             System.err.println("sun.stderr.encoding: " + System.getProperty("sun.stderr.encoding"));
392         }
393     }
394 }