バッファ範囲チェック
用意した箱より長いデータを書き込むと、あふれた分は消えてなくなるわけではありません。隣に置いてあった何かの上に乗ります。書いた側は何も気づかず、乗られた側だけが後で困ります。
はみ出した分は、隣の何かを書き換える
固定長のバッファは、メモリの上に取られた連続した領域です。長さ 10 の箱に 12 バイト書けば、後ろの 2 バイトは箱の外に出ます。そこに別の変数や、領域を管理するための情報が置かれていれば、それが書き換わります。
プレーンテキスト
長さ 10 の箱に 12 バイト書く
番号 0 1 2 3 4 5 6 7 8 9 | 10 11
<------ 箱の中 ----> <-- 外 -->厄介なのは、書いた瞬間には何も起きないことです。落ちるとしても、書き換えられた値が使われるずっと後で、まったく関係の無さそうな場所が壊れる形になります。ログを見ても書き込んだ側は正常に見えるので、原因までの距離が遠くなります。
範囲外がいつも大声で知らせてくれるとも限りません。切り出しは、頼んだ数に届かなくても黙って短い結果を返します。
Python
data = [0] * 10
print(len(data[8:12])) # 2 4 個頼んだのに 2 個しか返らない
print(data[-1]) # 末尾を指す。エラーにはならない負の番号も同じで、エラーにはならず反対側を指します。この 2 つが、範囲の間違いを静かに通してしまう入口です。
Python や JavaScript の配列は範囲外を弾いてくれますが、Buffer や TypedArray を扱うとき、切り出す範囲を自分で計算するときには、同じ話がそのまま出てきます。
触る場所は、offset から length 個ぶん
どこまで触るのかを、先にはっきりさせます。先頭から offset 番目に length バイト書くなら、実際に触るのは次の範囲です。
Python
buffer_size = 10
offset, length = 8, 4
for i in range(offset, offset + length):
print(i, "外" if i >= buffer_size else "内")
# 8 内 / 9 内 / 10 外 / 11 外最後に触るのは offset + length - 1 番です。箱の中にある番号は 0 から buffer_size - 1 までなので、この 2 つを見比べれば収まるかどうかが決まります。ここで 1 ずれると、ぴったり収まる書き込みを断ってしまうか、1 バイトだけはみ出す書き込みを通してしまうかのどちらかになります。
1 だけずれる間違いは off-by-one と呼ばれます。境目を扱う関数は、ぴったり収まるときと 1 だけはみ出すときの両方を必ずテストします。
通信で受け取ったデータを扱うときは、この計算の材料が外から来ます。ヘッダに書かれた長さをそのまま信じて本文を読み進めると、届いた分より先まで触ることになります。長さは、受け取った実際の大きさと突き合わせてから使います。
書く前に確かめる
あふれてから気づく方法はありません。書き込む前に確かめます。材料は、箱の大きさ、書き始める位置、書く長さの 3 つだけです。
見落としやすいのが負の数です。位置や長さに -1 が入ってくると、大小の比較だけをすり抜けます。長さがマイナスというのは意味を持たないので、比較に入る前に弾きます。逆に長さ 0 は何も触らないので、位置さえ箱の内側なら通してかまいません。
同じ判定が標準ライブラリに用意されていることもあります。Java の Objects.checkFromIndexSize や、C++ の std::span がそれです。用意されているなら、自分で書く前にそちらを使います。判定を自分で書くのは、その形が手の中にあるからこそ、ライブラリが何を守っているのかが読めるようになるためです。
要件
- isSafeWrite という名前の関数を実装すること
- offset と length が 0 以上であることを確認すること
- offset + length <= bufferSize なら true を返すこと
入出力例
isSafeWrite(10, 0, 10) → true
isSafeWrite(10, 5, 5) → true
isSafeWrite(10, 5, 6) → false
isSafeWrite(10, -1, 5) → false
isSafeWrite(10, 0, 0) → true
isSafeWrite(10, 0, -1) → false
isSafeWrite(10, 10, 0) → true