Обнаружил тут некоторый подводный камень в стандартной библиотеке Rust. Багом это назвать, конечно, нельзя, просто такой момент, где можно по собственной невнимательности наступить на грабли и не сразу это заметить, что нехарактерно для Rust.

Опасность подстерегает нас, когда мы читаем данные из файла посредством std::fs::File.read()1, не используя при этом std::io::BufReader2, а самостоятельно выделяя блок памяти и читая в него.

Подводный камень тут вот в чем, цитирую документацию:

if n is 0, then it can indicate one of two scenarios:

  • This reader has reached its “end of file” and will likely no longer be able to produce bytes. Note that this does not mean that the reader will always no longer be able to produce bytes.
  • The buffer specified was 0 bytes in length.

Подчеркивание мое. Итак, если мы передаем методу read() буфер нулевой длины, то результат будет ровно тот же самый, что и если мы достигли конца файла, т.е. Ok(0).

В каких случаях можно на это напороться? Мой пример (немного упрощенно) — для файла надо подсчитать контрольную сумму, считается она при помощи крейта crc64fast3, где метод Digest.write()4 берет на вход &[u8], почему собственно я и использую чтение в собственный буфер без прослоек. Выглядит это примерно так, для примера:

pub fn crc64(path: &PathBuf) -> io::Result<u64> {
  let mut file = fs::File::open(&path)?;
  let mut buffer = [0; 1024];
  let mut digest = Digest::new();
  loop {
    let l = file.read(&mut buffer)?;
    if l == 0 {
      break;
    }
    digest.write(&buffer[0..l]);
  }
  Ok(digest.sum64())
}

В примере я поместил статический буфер. Именно в таком виде и именно с таким циклом можно найти кучу примеров в сети. С ним все хорошо, кроме того, что размер задается при компиляции. Захотел я это дело параметризовать и напоролся на грабли. Просто потому что поначалу решил выделять память так5:

let mut buffer = Vec::<u8>::with_capacity(buffer_size);

(Слава турборыбе!) Ну, в самом деле, мне же нужно выделить память, а не проинициализировать ее — все равно перезапишется при чтении. В чем тут подвох? Подвох в том, что превращаясь в &mut [u8] такой вектор демонстрирует именно что нулевую длину. И никаких ошибок я при этом не получаю, просто все файлы почему-то кончаются, не успев начаться. Хорошо, что у меня была достаточно простая в целом задача, я логировал множество промежуточных моментов и заметил, что контрольные суммы как-то подозрительно одинаковые…

Пришлось переделать:

let mut buffer = vec![0; buffer_size];

Получил избыточную (в данном месте) инициализацию, но не очень представляю, как бы можно было без нее обойтись, не прибегая к unsafe (а прибегать к нему не хотелось бы из соображений чистоты кода).

Повторюсь, такое поведение стандартной библиотеки, безусловно, не баг. Но, на мой взгляд, просчет дизайна, который не помогает писать правильно, а наоборот — незаметно подталкивает к ошибке.

Лично я сделал бы передачу буфера нулевой длины ошибочной ситуацией (метод read() возвращает Result<usize>, так что с этим нет проблем), поскольку не вижу, в каких ситуациях это было бы нормально. Возможно, так сделано для симметрии с методами записи, где передача пустого буфера действительно может быть штатным случаем.