Parsing a u8 separated vector of FromStr from a BufRead
I'm calling the code directly from my main function, so I'm more
concerned with returning a human readable error rather than something
that can be caught and recovered from programmatically, hence I'm
using String as the error type.
[...]

Is it considered bad practice to return String as an error type?


Yes, it is generally considered bad practice to return Result&lt;_, String&gt; when the error returned is formatted from other errors.  The recommended approach is to define an enum and implement From, Display, and Error: (using the thiserror crate for simplicity)
use thiserror::Error;

#[derive(Debug, Error)]
pub enum Error {
    #[error(&quot;cannot read entry {entry_no}&quot;)]
    Io { entry_no: usize, source: std::io::Error },
    #[error(&quot;cannot read entry {entry_no}&quot;)]
    Encoding { entry_no: usize, source: std::string::FromUtf8Error },
    // ...
}


The above code works as far as I can tell, but I'm wondering if
there's a nicer way to deal with the mess in the match expression. The
functions from_utf8 and split return different error types, but
both implement Display so maybe there's some elegant way to
eliminate some code duplication there that I don't know about (I'm new
to rust, coming from C++).

Your options include:

methods on Option and Result, like Result::and_then and Option::ok_or_else;

the ? operator; and

using .zip(1..) instead of .enumerate() to get rid of that + 1,


plus potentially other things that didn't immediately come to mind.