© Fu Cheng 2018

Fu Cheng, Exploring Java 9, https://doi.org/10.1007/978-1-4842-3330-6_12

12. I/O

Fu Cheng

(1)Auckland, New Zealand

This chapter covers the changes to the java.io package in Java 9.

InputStream

The class java.io.InputStream adds three methods for reading and copying data from the input stream .

  • readAllBytes() : Reads all remaining bytes from the input stream.

  • readNBytes (byte[] b, int off, int len): Reads the given number of bytes from the input stream into the given byte array b. offset is the reading position, while len is the number of bytes to read.

  • transferTo (OutputStream out): Reads all bytes from the input stream and writes to the given output stream.

In Listing 12-1, the file input.txt contains the content “Hello World”. I use the methods readAllBytes() and readNBytes() to read data from the input stream and create strings from the data. I also use the method transferTo() to copy the data to a ByteArrayOutputStream.

Listing 12-1. Methods in InputStream for Reading and Copying Data
public class TestInputStream {

  private InputStream inputStream;
  private static final String CONTENT = "Hello World";


  @Before
  public void setUp() throws Exception {
    this.inputStream =
        TestInputStream.class.getResourceAsStream("/input.txt");
  }


  @Test
  public void testReadAllBytes() throws Exception {
    final String content = new String(this.inputStream.readAllBytes());
    assertEquals(CONTENT, content);
  }


  @Test
  public void testReadNBytes() throws Exception {
    final byte[] data = new byte[5];
    this.inputStream.readNBytes(data, 0, 5);
    assertEquals("Hello", new String(data));
  }


  @Test
  public void testTransferTo() throws Exception {
    final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    this.inputStream.transferTo(outputStream);
    assertEquals(CONTENT, outputStream.toString());
  }
}

Now that these three new methods have been added, you no longer need to use other third-party libraries like Apache Commons IO for some common usage scenarios.

The ObjectInputStream Filter

You can use Java object serialization to transform object graphs for persistence or network transfer. When deserializing the data to re-create the objects, make sure you check the input to make sure the data is valid; otherwise malicious content may compromise the system. You can set an implementation of the new interface java.io.ObjectInputFilter on an ObjectInputStream to filter the input.

ObjectInputFilter is a functional interface with only one method: ObjectInputFilter.Status checkInput(ObjectInputFilter.FilterInfo filterInfo). The interface ObjectInputFilter.FilterInfo contains information about the object being deserialized and the statistics of the ObjectInputStream object; see Table 12-1.

Table 12-1. Methods of ObjectInputFilter. FilterInfo

Method

Description

Class<?> serialClass()

Returns the class of the object being deserialized

long arrayLength()

If the object being deserialized is an array, returns the length of the array

long depth()

Returns the depth of the current object in the object graph

long references()

Returns the current number of object references

long streamBytes()

Returns the current number of bytes consumed

The return value of checkInput() is the enum ObjectInputFilter.Status; see Table 12-2.

Table 12-2. Values of ObjectInputFilter. Status

Status

Description

ALLOWED

The object is allowed to be deserialized.

REJECTED

The object is not allowed to be deserialized.

UNDECIDED

This filter doesn’t decide whether the object is allowed to be deserialized.

An ObjectInputFilter should only check for the condition that it’s designed for and return ALLOWED or REJECTED only when it knows the result; otherwise it should return UNDECIDED to let other filters decide the result. If REJECTED is returned as the status, then the ObjectInputStream throws the java.io.InvalidClassException.

ObjectInputFilters can be set on individual ObjectInputStream objects using the new method setObjectInputFilter(). You can also set a process-wide filter using the static method setSerialFilter(ObjectInputFilter filter) of the class ObjectInputFilter.Config. If the individual ObjectInputFilter is not set, then the process-wide filter will be applied.

Another important feature of ObjectInputFilter.Config is that it can create ObjectInputFilters from string patterns. String patterns are much easier to write than an ObjectInputFilter interface is to implement. A filter can have multiple patterns separated by semicolons (;). A pattern can set a limit or specify the class name to match. The following limits are supported:

  • maxdepth=value: The maximum depth of a graph

  • maxrefs=value: The maximum number of object references

  • maxbytes=value: The maximum number of bytes in the input stream

  • maxarray=value: The maximum length of arrays

For example, you can use maxdepth=5 to limit the maximum depth of a graph to 5.

Other patterns match the class names. Here are some scenarios:

  • If the pattern contains “/”, then it specifies both the module name and the class name to match.

  • If the pattern ends with ".**", then it matches any class in the package and subpackages.

  • If the pattern ends with ".*", then it matches any class in the package.

  • If the pattern ends with "*", then it matches any class with the pattern as the prefix.

  • If the pattern is equal to the class name, then it matches the class name.

  • If the pattern starts with "!", then it negates the pattern after it.

Listing 12-2 shows some examples of using ObjectInputFilters to filter input streams. Nested classes A, B, C, and D are used to demonstrate an object graph. In the method setUp(), I create a new instance of class A and use ObjectOutputStream to get the serialized bytes. objectInput is the ByteArrayInputStream object for the serialized data. In the test case testFilterByClass, I check to make sure that objects of class B do not exist in the input stream. I expect the exception InvalidClassException to be thrown. In the test case testFilterByDepth, I check to make sure the maximum depth of object graphs is not greater than 6. In the test case testProcessWideFilter, I configure a process-wide filter to check for class C. In the test case testFilterPattern, I use the pattern "!io.vividcode.feature9.** " to specify that all classes in the package io.vividcode.feature9 and its subpackages are not allowed.

Listing 12-2. Using ObjectInputFilter to Filter Input Streams
public class TestObjectInputFilter {

  static class A implements Serializable {

    public int a = 1;
    public B b = new B();
  }


  static class B implements Serializable {

    public String b = "hello";
    public C c = new C();
  }


  static class C implements Serializable {

    public int c = 2;
    public D[] ds = new D[]{new D(), new D()};
  }


  static class D implements Serializable {

    public String d = "world";
  }


  private ByteArrayInputStream objectInput;

  @Before
  public void setUp() throws Exception {
    final A a = new A();
    final ByteArrayOutputStream baos = new ByteArrayOutputStream();
    final ObjectOutputStream outputStream = new ObjectOutputStream(baos);
    outputStream.writeObject(a);
    this.objectInput = new ByteArrayInputStream(baos.toByteArray());
  }


  @Test(expected = InvalidClassException.class)
  public void testFilterByClass() throws Exception {
    final ObjectInputStream inputStream = new ObjectInputStream(this.objectInput);
    inputStream.setObjectInputFilter(filterInfo -> {
      if (B.class.isAssignableFrom(filterInfo.serialClass())) {
        return ObjectInputFilter.Status.REJECTED;
      }
      return ObjectInputFilter.Status.UNDECIDED;
    });
    final A a = (A) inputStream.readObject();
    assertEquals(1, a.a);
  }


  @Test
  public void testFilterByDepth() throws Exception {
    final ObjectInputStream inputStream = new ObjectInputStream(this.objectInput);
    inputStream.setObjectInputFilter(filterInfo -> {
      if (filterInfo.depth() > 6) {
        return ObjectInputFilter.Status.REJECTED;
      }
      return ObjectInputFilter.Status.UNDECIDED;
    });
    final A a = (A) inputStream.readObject();
    assertEquals(1, a.a);
  }


  @Test(expected = InvalidClassException.class)
  public void testProcessWideFilter() throws Exception {
    ObjectInputFilter.Config.setSerialFilter(filterInfo -> {
      if (C.class.isAssignableFrom(filterInfo.serialClass())) {
        return ObjectInputFilter.Status.REJECTED;
      }
      return ObjectInputFilter.Status.UNDECIDED;
    });
    final ObjectInputStream inputStream = new ObjectInputStream(this.objectInput);
    final A a = (A) inputStream.readObject();
    assertEquals(1, a.a);
  }


  @Test(expected = InvalidClassException.class)
  @Ignore
  public void testFilterPattern() throws Exception {
    ObjectInputFilter.Config.setSerialFilter(
        ObjectInputFilter.Config.createFilter("!io.vividcode.feature9.**"));
    final ObjectInputStream inputStream = new ObjectInputStream(this.objectInput);
    final A a = (A) inputStream.readObject();
    assertEquals(1, a.a);
  }
}

The process-wide filter can be set exactly once. Trying to set it again causes the IllegalStateException. That’s why the test case testFilterPattern is marked as ignored. If it weren’t, the test case would fail with the IllegalStateException because the process-wide filter is already set in the test case testProcessWideFilter. When running the test cases using Maven, you can configure the Maven Surefire plugin to fork the JVM for each test method; then you can run both test cases successfully.

Summary

In this chapter, we discussed changes related to I/O in Java 9, including new methods in InputStream and ObjectInputStream filters. In the next chapter, we’ll discuss changes related to security in Java 9.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.15.129.90