Prevent infinite loop with AbstractCharacterFilterReader if EOF is
filtered out
Based on PR by Rob Spoor, with an additional (missing) test, and
clean ups.
Add CharacterSetFilterReader.CharacterSetFilterReader(Reader,
Integer...).
MODIFY
src/changes/changes.xml
src/changes/changes.xml
@@ -123,6 +123,9 @@ The <action> type attribute can be add,update,fix,remove.
<action issue="IO-720" dev="ggregory" type="fix" due-to="XenoAmess">
Fix error about usage of DirectBuffer in JRE 16/17 #205.
</action>
+ <action dev="ggregory" type="fix" due-to="Rob Spoor, Gary Gregory">
+ Prevent infinite loop with AbstractCharacterFilterReader if EOF is filtered out #226.
+ </action>
<!-- ADD -->
<action dev="ggregory" type="add" due-to="Gary Gregory">
Add FileSystemProviders class.
@@ -196,6 +199,9 @@ The <action> type attribute can be add,update,fix,remove.
<action issue="IO-651" dev="ggregory" type="add" due-to="jmark109, Gary Gregory">
Add DeferredFileOutputStream.toInputStream() #206.
</action>
+ <action dev="ggregory" type="add" due-to="Gary Gregory">
+ Add CharacterSetFilterReader.CharacterSetFilterReader(Reader, Integer...).
+ </action>
<!-- UPDATES -->
<action dev="ggregory" type="update" due-to="Dependabot">
Update junit-jupiter from 5.6.2 to 5.7.0 #153.

MODIFY
src/main/java/org/apache/commons/io/input/AbstractCharacterFilterReader.java
src/main/java/org/apache/commons/io/input/AbstractCharacterFilterReader.java
/**
* Constructs a new reader.
*
- * @param reader
- * the reader to filter
+ * @param reader the reader to filter
*/
protected AbstractCharacterFilterReader(final Reader reader) {
super(reader);
@@ -42,15 +41,14 @@ public int read() throws IOException {
int ch;
do {
ch = in.read();
- } while (filter(ch));
+ } while (ch != EOF && filter(ch));
return ch;
}
/**
* Returns true if the given character should be filtered out, false to keep the character.
*
- * @param ch
- * the character to test.
+ * @param ch the character to test.
* @return true if the given character should be filtered out, false to keep the character.
*/
protected abstract boolean filter(int ch);

MODIFY
src/main/java/org/apache/commons/io/input/CharacterSetFilterReader.java
src/main/java/org/apache/commons/io/input/CharacterSetFilterReader.java
package org.apache.commons.io.input;
import java.io.Reader;
+import java.util.Arrays;
import java.util.Collections;
+import java.util.HashSet;
import java.util.Set;
/**
- * A filter reader that removes a given set of characters represented as {@code int} code points, handy to remove
- * known junk characters from CSV files for example.
+ * A filter reader that removes a given set of characters represented as {@code int} code points, handy to remove known
+ * junk characters from CSV files for example.
* <p>
- * This class must convert each {@code int} read to an {@code Integer}. You can increase the Integer cache
- * with a system property, see {@link Integer}.
+ * This class must convert each {@code int} read to an {@code Integer}. You can increase the Integer cache with a system
+ * property, see {@link Integer}.
* </p>
*/
public class CharacterSetFilterReader extends AbstractCharacterFilterReader {
/**
* Constructs a new reader.
*
- * @param reader
- * the reader to filter.
- * @param skip
- * the set of characters to filter out.
+ * @param reader the reader to filter.
+ * @param skip the set of characters to filter out.
+ * @since 2.9.0
+ */
+ public CharacterSetFilterReader(final Reader reader, final Integer... skip) {
+ this(reader, new HashSet<>(Arrays.asList(skip)));
+ }
+
+ /**
+ * Constructs a new reader.
+ *
+ * @param reader the reader to filter.
+ * @param skip the set of characters to filter out.
*/
public CharacterSetFilterReader(final Reader reader, final Set<Integer> skip) {
super(reader);

MODIFY
src/test/java/org/apache/commons/io/input/CharacterFilterReaderTest.java
src/test/java/org/apache/commons/io/input/CharacterFilterReaderTest.java
*/
package org.apache.commons.io.input;
+import static org.apache.commons.io.IOUtils.EOF;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
import java.io.IOException;
import java.io.StringReader;
+import java.time.Duration;
import java.util.HashSet;
import org.apache.commons.io.IOUtils;
public class CharacterFilterReaderTest {
+ private static final String STRING_FIXTURE = "ababcabcd";
+
@Test
public void testInputSize0FilterSize1() throws IOException {
final StringReader input = new StringReader("");
@@ -72,9 +77,24 @@ public void testInputSize2FilterSize1FilterLast() throws IOException {
}
}
+ @Test
+ public void testReadFilteringEOF() throws IOException {
+ final StringReader input = new StringReader(STRING_FIXTURE);
+ assertTimeoutPreemptively(Duration.ofMillis(500), () -> {
+ try (StringBuilderWriter output = new StringBuilderWriter();
+ CharacterFilterReader reader = new CharacterFilterReader(input, EOF)) {
+ int c;
+ while ((c = reader.read()) != EOF) {
+ output.write(c);
+ }
+ assertEquals(STRING_FIXTURE, output.toString());
+ }
+ });
+ }
+
@Test
public void testReadIntoBuffer() throws IOException {
- final StringReader input = new StringReader("ababcabcd");
+ final StringReader input = new StringReader(STRING_FIXTURE);
try (CharacterFilterReader reader = new CharacterFilterReader(input, 'b')) {
final char[] buff = new char[9];
final int charCount = reader.read(buff);
@@ -85,8 +105,9 @@ public void testReadIntoBuffer() throws IOException {
@Test
public void testReadUsingReader() throws IOException {
- final StringReader input = new StringReader("ababcabcd");
- try (StringBuilderWriter output = new StringBuilderWriter(); CharacterFilterReader reader = new CharacterFilterReader(input, 'b')) {
+ final StringReader input = new StringReader(STRING_FIXTURE);
+ try (StringBuilderWriter output = new StringBuilderWriter();
+ CharacterFilterReader reader = new CharacterFilterReader(input, 'b')) {
IOUtils.copy(reader, output);
assertEquals("aacacd", output.toString());
}

MODIFY
src/test/java/org/apache/commons/io/input/CharacterSetFilterReaderTest.java
src/test/java/org/apache/commons/io/input/CharacterSetFilterReaderTest.java
*/
package org.apache.commons.io.input;
+import static org.apache.commons.io.IOUtils.EOF;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
import java.io.IOException;
import java.io.StringReader;
+import java.time.Duration;
import java.util.HashSet;
+import java.util.Set;
+import org.apache.commons.io.output.StringBuilderWriter;
import org.junit.jupiter.api.Test;
public class CharacterSetFilterReaderTest {
+ private static final String STRING_FIXTURE = "ab";
+
@Test
public void testInputSize0FilterSize0() throws IOException {
final StringReader input = new StringReader("");
@@ -47,7 +54,7 @@ public void testInputSize0FilterSize1() throws IOException {
@Test
public void testInputSize0NullFilter() throws IOException {
final StringReader input = new StringReader("");
- try (CharacterSetFilterReader reader = new CharacterSetFilterReader(input, null)) {
+ try (CharacterSetFilterReader reader = new CharacterSetFilterReader(input, (Set<Integer>) null)) {
assertEquals(-1, reader.read());
}
}
@@ -75,7 +82,7 @@ public void testInputSize2FilterSize1FilterAll() throws IOException {
@Test
public void testInputSize2FilterSize1FilterFirst() throws IOException {
- final StringReader input = new StringReader("ab");
+ final StringReader input = new StringReader(STRING_FIXTURE);
final HashSet<Integer> codePoints = new HashSet<>();
codePoints.add(Integer.valueOf('a'));
try (CharacterSetFilterReader reader = new CharacterSetFilterReader(input, codePoints)) {
@@ -86,7 +93,7 @@ public void testInputSize2FilterSize1FilterFirst() throws IOException {
@Test
public void testInputSize2FilterSize1FilterLast() throws IOException {
- final StringReader input = new StringReader("ab");
+ final StringReader input = new StringReader(STRING_FIXTURE);
final HashSet<Integer> codePoints = new HashSet<>();
codePoints.add(Integer.valueOf('b'));
try (CharacterSetFilterReader reader = new CharacterSetFilterReader(input, codePoints)) {
@@ -97,7 +104,7 @@ public void testInputSize2FilterSize1FilterLast() throws IOException {
@Test
public void testInputSize2FilterSize2FilterFirst() throws IOException {
- final StringReader input = new StringReader("ab");
+ final StringReader input = new StringReader(STRING_FIXTURE);
final HashSet<Integer> codePoints = new HashSet<>();
codePoints.add(Integer.valueOf('a'));
codePoints.add(Integer.valueOf('y'));
@@ -109,7 +116,7 @@ public void testInputSize2FilterSize2FilterFirst() throws IOException {
@Test
public void testInputSize2FilterSize2FilterLast() throws IOException {
- final StringReader input = new StringReader("ab");
+ final StringReader input = new StringReader(STRING_FIXTURE);
final HashSet<Integer> codePoints = new HashSet<>();
codePoints.add(Integer.valueOf('x'));
codePoints.add(Integer.valueOf('b'));
@@ -121,7 +128,7 @@ public void testInputSize2FilterSize2FilterLast() throws IOException {
@Test
public void testInputSize2FilterSize2FilterNone() throws IOException {
- final StringReader input = new StringReader("ab");
+ final StringReader input = new StringReader(STRING_FIXTURE);
final HashSet<Integer> codePoints = new HashSet<>();
codePoints.add(Integer.valueOf('x'));
codePoints.add(Integer.valueOf('y'));
@@ -130,4 +137,19 @@ public void testInputSize2FilterSize2FilterNone() throws IOException {
assertEquals('b', reader.read());
}
}
+
+ @Test
+ public void testReadFilteringEOF() throws IOException {
+ final StringReader input = new StringReader(STRING_FIXTURE);
+ assertTimeoutPreemptively(Duration.ofMillis(500), () -> {
+ try (StringBuilderWriter output = new StringBuilderWriter();
+ CharacterSetFilterReader reader = new CharacterSetFilterReader(input, EOF)) {
+ int c;
+ while ((c = reader.read()) != EOF) {
+ output.write(c);
+ }
+ assertEquals(STRING_FIXTURE, output.toString());
+ }
+ });
+ }
}

Prevent infinite loop with AbstractCharacterFilterReader if EOF is
filtered out #226.

- Based on PR #226 by Rob Spoor, with an additional (missing) test, and
clean ups.
- Add CharacterSetFilterReader.CharacterSetFilterReader(Reader,
Integer...).