Skip to content

Latest commit

 

History

History
146 lines (106 loc) · 5.32 KB

File metadata and controls

146 lines (106 loc) · 5.32 KB

06. エラーと安全上限

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


1. ErrorCode

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

使い分けの考え方

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

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

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


2. 言語ごとの表現

言語 表現
C# SpringNbtException : ExceptionCode プロパティ、InnerException に原因)
Java SpringNbtException extends RuntimeExceptioncode()getCause() に原因)
TypeScript SpringNbtError extends Errorcodecause に原因)
Python SpringNbtError(Exception)code 属性、__cause__ に原因)
Rust Result<T, Error>Errorcode()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 は検査例外を使わない

IOExceptionErrorCode.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. 次に読むもの