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