Load configuration with mapped errors

Rust
use std::collections::HashMap;
use std::fs;

#[derive(Debug)]
enum ConfigError {
    Read(std::io::Error),
    Invalid { line: usize, message: String },
}

/// Load "key=123" pairs from a file, reporting the line number on bad input.
fn load_config(path: &str) -> Result<HashMap<String, u16>, ConfigError> {
    let text = fs::read_to_string(path).map_err(ConfigError::Read)?;
    let mut config = HashMap::new();
    for (index, raw) in text.lines().enumerate() {
        let line = raw.trim();
        if line.is_empty() || line.starts_with('#') {
            continue;
        }
        let Some((key, value)) = line.split_once('=') else {
            return Err(ConfigError::Invalid {
                line: index + 1,
                message: "expected key=value".to_string(),
            });
        };
        let port = value.trim().parse::<u16>().map_err(|_| ConfigError::Invalid {
            line: index + 1,
            message: format!("{key} is not a valid port"),
        })?;
        config.insert(key.trim().to_string(), port);
    }
    Ok(config)
}

fn main() {
    match load_config("app.conf") {
        Ok(config) => println!("{:?}", config),
        Err(ConfigError::Read(error)) => eprintln!("cannot read config: {error}"),
        Err(ConfigError::Invalid { line, message }) => eprintln!("line {line}: {message}"),
    }
}