2015-07-03 [長年日記]

MalformedInputException [java][Scala]

UTF-8として不正なファイルを作ります。

cat相当のJavaプログラムを作ります。

import java.io.*;

public class cat {
    public static void main(String[] args) {
        try {
            BufferedReader r = new BufferedReader(
                new InputStreamReader(
                    System.in
                )
            );
            String line;
            while ((line = r.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

class名が小文字だけど気にしない。

javac cat.java
cat malformed.txt | java cat
あ�お

と、不完全なシークェンスが出力されて終わります。


cat相当のscalaプログラムを作ります。

import scala.io.Source
try {
  Source.stdin.getLines.foreach{x=>
    println(x)
  }
} catch {
  case e : java.nio.charset.MalformedInputException => {
    e.printStackTrace
  }
}


cat malformed.txt | scala cat.scala
java.nio.charset.MalformedInputException: Input length = 2
	at java.nio.charset.CoderResult.throwException(CoderResult.java:281)
	at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:339)
	at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
	at java.io.InputStreamReader.read(InputStreamReader.java:184)
	at java.io.BufferedReader.fill(BufferedReader.java:161)
	at java.io.BufferedReader.readLine(BufferedReader.java:324)
	at java.io.BufferedReader.readLine(BufferedReader.java:389)
	at scala.io.BufferedSource$BufferedLineIterator.hasNext(BufferedSource.scala:72)
	at scala.collection.Iterator$class.foreach(Iterator.scala:743)
	at scala.collection.AbstractIterator.foreach(Iterator.scala:1195)
	at Main$$anon$1.<init>(cat.scala:3)
	at Main$.main(cat.scala:1)
:
:

つらい。


なにがつらいかというと、エラーが発生した時の出力結果=正常にprintlnできた最後の行が実行するたびに変わってしまうこと。
つまり、エラーが発生したと思われる行を確認しても何も問題がないように見えるということです。

なんでだろうかと色々と調べて考えて、処理系のバグでも踏んだのだろうかとまで思いました。
違いました。
BufferedReaderが先読みをしているとき、その先読みをしている箇所でエラーが発生しているのです。
なので実際に正しくないバイトシークェンスは、正常にprintlnできた行よりももっともっと後ろの方に存在したのでした。
面倒だなぁ。


(というメモ。何か判明したか追記する)


一応追記

エラーがあるバイトシークェンスに対しての振る舞いの規定については
java.nio.charset.CodingErrorAction というクラスで定数が定義されている。この設定がJavaとScalaで規定値が違うらしい。

javaなら、

import java.io.*;
import java.nio.charset.*;

public class cat {
    public static void main(String[] args) {
        try {
            CharsetDecoder d = Charset.forName("UTF-8").newDecoder();
            d.onMalformedInput(CodingErrorAction.REPORT);
            d.onUnmappableCharacter(CodingErrorAction.REPORT);
            BufferedReader r = new BufferedReader(
                new InputStreamReader(
                    System.in,
                    d
                )
            );
            String line;
            while ((line = r.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

として、InputStreamReaderの第2引数を指定するとScalaの様に例外を生成する。


scalaなら、scala.io.Codecを使って、

import java.nio.charset.CodingErrorAction
import scala.io.{Codec, Source}

implicit val codec = Codec("UTF-8")
codec.onMalformedInput(CodingErrorAction.REPLACE)
codec.onUnmappableCharacter(CodingErrorAction.REPLACE)

try {
  Source.fromInputStream(System.in).getLines.foreach(println)
} catch {
  case e : java.nio.charset.MalformedInputException => {
    e.printStackTrace
  }
}

として、Codecの振る舞いをすげ替えてやるとJavaのような挙動になる。

  Source.fromInputStream(System.in)(codec).getLines.foreach(println)

として明に指定してもよい。


CodingErrorAction.IGNORE を指定すると不正なバイトシークェンスは全部捨てる。


ついで
置換先の文字を変更する。

import java.nio.charset.{CharsetDecoder, Charset, CodingErrorAction}
import scala.io.{Codec, Source}
val d = Charset.forName("UTF-8").newDecoder()
d.replaceWith("〓")
d.onMalformedInput(CodingErrorAction.REPLACE)
d.onUnmappableCharacter(CodingErrorAction.REPLACE)
implicit val codec = Codec(d)