Description
The documentation of write
(emphasis added) specifies that returning Ok(0)
is a valid thing to do, albeit with a possible implementation-specific "this writer is dead" meaning:
A return value of 0 typically means that the underlying object is no longer able to accept bytes and will likely not be able to in the future as well, or that the buffer provided is empty.
Despite the above, Write::write_all
's current implementation returns an error when an underlying write
returns Ok(0)
, effectively making this a forbidden return value for implementations of Write
. However, in cases where a Write
is transforming or buffering data, it can be useful to return Ok(0)
to avoid violating this other, more firmly worded, requirement of write
:
A call to
write
represents at most one attempt to write to any wrapped object.
I came across this issue while working on a streaming base64 encoder Write
for the base64
crate, and subsequently found other cases where these features have clashed (hat tip to jebrosen
on IRC).
In the case of base64, suppose a user constructs a base64 writer with an internal buffer that delegates to, say, stdout. The user calls write
with a 6-byte input, which is base64 encoded into 8 bytes in the internal buffer, and subsequently written to the delegate (stdout). The delegate writes 5 bytes, and the remaining 3 bytes are kept in the buffer. On the next call to write
from the user, I want to consume no input (and therefore return Ok(0)
), but rather simply try again to write the remaining 3 bytes to the delegate. (Even with a different implementation that does consume as much input as can be encoded and still fit within the buffer, eventually that buffer will fill, and we're back to returning Ok(0)
.)
The same issue affects flate2:
// miniz isn't guaranteed to actually write any of the buffer provided,
// it may be in a flushing mode where it's just giving us data before
// we're actually giving it any data. We don't want to spuriously return
// `Ok(0)` when possible as it will cause calls to write_all() to fail.
// As a result we execute this in a loop to ensure that we try our
// darndest to write the data.
That violates the "at most one write" part of write
, but it could be avoided if returning Ok(0)
wasn't going to break write_all
.
BufWriter
in the std lib has a similar problem:
impl<W: Write> Write for BufWriter<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
if self.buf.len() + buf.len() > self.buf.capacity() {
self.flush_buf()?;
}
if buf.len() >= self.buf.capacity() {
self.panicked = true;
let r = self.inner.as_mut().unwrap().write(buf);
self.panicked = false;
r
} else {
Write::write(&mut self.buf, buf)
}
}
...
If BufWriter::write
is called when its buffer is nonempty with an input that won't fit in the buffer, the second if
leads to two calls to the delegate writer for one BufWriter::write
call, which violates the contract of write
. Similarly, returning Ok(0)
there would solve the issue: the caller could then retry with the same buffer, which would be passed to the delegate writer untouched.
No doubt there are complexities I don't see yet, but given that write_all
's documentation doesn't say anything about erroring on Ok(0)
, is it feasible to simply continue looping instead of erroring on Ok(0)
? If the concept of "writer exhaustion" the "typically..." clause in write
's docs is referring to needs to be represented, could that not be an ErrorKind
?