I've used .write() explicitly outside of a loop in the context of Linux kernel interfaces that require you send the entire payload in a single write syscall, and a retry to send the remainder on a failure to read the whole thing isn't the right move (those APIs usually promise to write the entire buffer, but better to report an error on something unexpected happening than writing who knows what that could cause who knows what to happen).
I think having a lint for calling .write() outside of a retry loop or other .write() function implementation would be a good idea (even warn-by-default as long as I can #[allow] it), but I think anything beyond that to make it harder to call .write() would be too much.
Technically File::write doesn't guarantee that it will attempt to write the entire buffer. In practice for as long as your buffer is smaller than c_int::MAX (Apple) or ssize_t::MAX it will not truncate the write: https://github.com/rust-lang/rust/blob/ccc9ba5c30c675824e9ca62b960830ff4a1858ea/library/std/src/sys/pal/unix/fd.rs#L307 Anything write bigger than ssize_t::MAX results in unspecified behavior according to POSIX. And on Apple systems there is a bug that causes any write bigger than c_int::MAX to return an error. So to ensure forward-progress we are forced to truncate the write.
That's absolutely a valid use case. For that, I would personally suggest an extension trait in a library crate:
/// Extension trait for the `single_write` function
pub trait SingleWrite: std::io::Write {
/// Perform a single write syscall and error on partial writes.
///
/// For use on special OS files that specifically require no partial writes,
/// and should always accept a full write.
fn single_write(&mut self, buf: &[u8]) -> std::io::Result<()> {
if self.write(buf)? != buf.len() {
Err(std::io::Error::from(std::io::ErrorKind::UnexpectedEof))
} else {
Ok(())
}
}
}
impl<W: std::io::Write> SingleWrite for W {}
Then, if we add a lint about using Write::write outside of a loop or another impl of Write, I'd add an expect for that lint on single_write.
@@
expression buffer, towrite;
@@
- buffer.write(towrite);
+ let len = buffer.write(towrite);
+ if len < total_len { try_again(); }
This checks for any .write s which are used as a statement and their values are discarded. Surely this would not catch all the instances, but maybe one could come up with more common patterns like this which users may write.
The above semantic patch would also not bother with cases where the result of .write is not thrown away.
Edit: Atleast it can be used to check for any wrong uses of write()
BTW. Just today I was remainded in an unrelated case, that ability to rename a symbol without making it a breaking change (old name would become an alias, possibly marked as deprecated), would be very, very useful. With structs and traits it's possible to do it with type aliases, for macros and modules with a blanket forwarding, but for methods with a forwarding method, but for trait methods and struct fields there's nothing.