Skip to content

Latest commit

 

History

History
138 lines (98 loc) · 5.39 KB

06-errors-and-limits.md

File metadata and controls

138 lines (98 loc) · 5.39 KB

06. エラーと安全上限

エラーの分類は全言語で完全に一致します。 言語ごとに違うのは、エラーを例外で投げるかResultで返すかだけです。


1. ErrorCode

コード 意味 例
IO 入出力の失敗 ファイルが無い、権限が無い
MALFORMED_DATA バイト列が仕様に反する 未知のタグID、途中で入力が尽きた
UNEXPECTED_TAG_TYPE 期待した型と違うタグを取り出した GetInt("x")にStringが入っていた
UNSUPPORTED_FEATURE 仕様上は妥当だが本ビルドで扱えない 圧縮ID=127のチャンクをReadChunkで読んだ、圧縮ID=4(LZ4)を指定してチャンクを書こうとした
LIMIT_EXCEEDED 安全上限を超えた ネスト深さ512超
INVALID_ARGUMENT 呼び出し側の引数が不正 座標がチャンク範囲外、i8に300を渡した
UNSUPPORTED_DATA_VERSION 扱える形式より古いデータ DataVersion 4786未満のチャンクを既定設定で書き戻そうとした

使い分けの考え方

MALFORMED_DATAとINVALID_ARGUMENTの境目は「誰の間違いか」で決まります。

  • ファイルの中身がおかしい → MALFORMED_DATA
  • 呼び出し側が渡した値がおかしい → INVALID_ARGUMENT

たとえばBlockState.Parse("minecraft:stone[")は、呼び出し側が渡した文字列の誤りなのでINVALID_ARGUMENTになります。


2. 言語ごとの表現

言語 表現
C# SpringNbtException : Exception(Codeプロパティ、InnerExceptionに原因)
Java SpringNbtException extends RuntimeException(code()、getCause()に原因)
TypeScript SpringNbtError extends Error(code、causeに原因)
Python SpringNbtError(Exception)(code属性、__cause__に原因)
Rust Result<T, Error>(Errorはcode()とmessage()を持つ)
try
{
    NbtIo.ReadFile(path);
}
catch (SpringNbtException error) when (error.Code == ErrorCode.MalformedData)
{
    // 破損したファイルとして扱う
}
match read_file(path, &NbtReadOptions::default()) {
    Ok(named) => { /* ... */ }
    Err(error) if error.code() == ErrorCode::MalformedData => { /* ... */ }
    Err(error) => return Err(error),
}

Javaは検査例外を使わない

IOExceptionはErrorCode.IOで包んで非検査例外として投げます。 全言語でメソッドのシグネチャを揃えるためです(adr/0005)。 原因の例外はgetCause()から取れるので、情報は失われません。


3. 安全上限

信用できないNBTファイルを読むときに、悪意ある入力で無制限にメモリを確保させられないよう、上限を設けています。

項目 既定値 超過時
ネスト深さ 512 LIMIT_EXCEEDED
配列・リストの要素数 制限なし(宣言長 > 残り入力長なら即エラー) MALFORMED_DATA
展開後の総バイト数 制限なし(設定可) LIMIT_EXCEEDED
NbtReadOptions options = new NbtReadOptions
{
    MaxDepth = 64,
    MaxDecompressedSize = 16 * 1024 * 1024,
};

宣言長は確保前に検証する

TAG_Byte_Arrayの長さに0x7FFFFFFFと書いてあっても、実際の残り入力がそれに足りなければ、確保する前にエラーにします。 「2GB確保してから足りないと気づく」という動きはしません。


4. Rustのスタックに注意

読み込み・書き出し・SNBTはいずれも再帰で実装しています。 既定の深さ512はどの言語でも安全に扱えますが、Rustだけは事情があります。

debugビルドでは1段あたり約8 KBを使います(releaseでは約1 KB)。 512段で4 MBになり、既定のスレッドスタックによっては足りません。

深いデータを扱うなら、大きめのスタックを持つスレッドで走らせてください。

std::thread::Builder::new()
    .stack_size(32 * 1024 * 1024)
    .spawn(|| { /* 深いNBTを読む */ })?
    .join()
    .unwrap();

Pythonも既定の再帰上限(1000)では深さ512に届きませんが、ライブラリ側で一時的に引き上げているので、利用者の対応は不要です。

詳細はspec/00 5.1。


5. 破損したデータを推測で取り繕わない

破損したデータを推測で修復することはしません。

たとえばパレットの添字が範囲外を指しているとき、0番目で代替すれば「読める」ようにはなります。 しかし、それを書き戻すと、破損したデータが破損していないように見える形で保存されてしまいます。

そこで、明示的にMALFORMED_DATAを返します。

例外は1つだけで、第三者ツールが書いた非正準なBitStorageを救済するためのlenient_bit_storageオプションがあります。 このオプションは明示的に有効にしたときだけ働きます。

ChunkReadOptions options = new ChunkReadOptions { LenientBitStorage = true };

6. 次に読むもの