DOSContainer logo DOSContainer

Loading configuration files in Rust

📅 2025-05-11  · ✍️ Bas v.d. Wiel  ·  🏷 design

Loading and parsing configuration files is a key component of DOSContainer. The chosen file format for configuration is TOML, for simplicity. Rust’s community produced a crate specifically to read and write this format, so yay! But unfortunately for me, this is far from ideal. One of my project’s base principles is that I only want to write code if I absolutely have to. If I can borrow functionality from a community crate, that’s what I’ll do.

To get from a TOML file to a data structure we can use to build a disk image, there’s a number of steps that must be taken:

  1. Tell the program which file to read.
  2. Read the contents of the file into a String.
  3. Parse the String, assuming it’s valid TOML.
  4. Construct DOSContainer-internal data structures from the TOML.

Things can obviously go wrong at each of these steps, so we handle errors properly along the way. Unfortunately there’s preciously little we can really do if the input file is wrong in some unknown way, so an error usually means we quite with a message.

Tell the program which file to read

Since the first user interface to DOSContainer will be a command line utility, it will need to accept parameters. I’ve been told that the clap crate is overkill for that, but it does seem to work just fine. Ok, so I’m getting a relatively big binary, but it’s what I’m working with in this phase of the project.

#[derive(Subcommand)]
enum Commands {
    /// Build a DOSContainer manifest into a disk image.
    Build {
        /// Filename of the HwSpec configuration file, this a TOML file.
        hwspec: PathBuf,
        /// Filename of the build manifest.
        manifest: PathBuf,
    },
    /// Build a Collection of DOSContainer manifests into a library of disk images.
    BuildCollection { startdir: PathBuf },
    /// Download all dependencies required to host your own DOSContainer collections.
    SelfHost {
        /// Directory on your local machine that'll hold DOSContainer assets.
        docroot: PathBuf,
    },
}

This snippet of completely draft code serves to make the Build command accept two positional arguments that point to a file. The type PathBuf ensures that the parameters get treated as file system paths.

Commands::Build { hwspec, manifest: _ } => {
  // Construct a HW Spec from a TOML file
  let toml_string = fs::read_to_string(hwspec)?;
  let hwspec = HwSpec::from_toml(&toml_string).expect("BOOM!");
  println!("{}", hwspec);

Reading the file itself

This incomplete snippet serves to illustrate some of the steps taken. A TOML string toml_string is generated by reading the file as a string. If that fails, an error gets thrown by virtue of the ? operator.

The next line instantiates an actual HwSpec struct instance. The .expect("BOOM!"); bit is there to save me the hassle of implementing error handling (this is all throwaway code for now). It will go out with a boom if something goes wrong. If it succeeds, however, we’ll be the proud owners of a new HwSpec instance which tells DOSContainer what the properties of the target PC system are that we’re building a collection for.

Parsing a string as if it were TOML

Lots of the proverbial magic happens in the from_toml() method on the HwSpec struct. Let’s look a little closer at that.

pub fn from_toml(toml_string: &str) -> Result<Self, HwSpecError> {
    let settings = Config::builder()
        .add_source(File::from_str(toml_string, FileFormat::Toml))
        .build()
        .map_err(HwSpecError::ConfigBuild)?;

    settings
        .try_deserialize::<HwSpec>()
        .map_err(HwSpecError::Deserialize)
    }

You’ll notice that we’re calling Config::builder() here. That part comes from the config crate, which does just about all of the heavy lifting for me together with serde. The config crate was built specifically for the purpose of handling configuration settings from all sorts of sources. It can handle files directly, so I’ll probably be refactoring that bit later on. For now, I’m passing in a string that contains the contents of my file. The source is a File::from_str(toml_string, FileFormat::Toml)), constructing what the config crate calls a ‘file’.

The config crate constructs a ConfigBuilder first. If that succeeds, we’ll know that we got a valid TOML file that was usable to Rust. In the next step we try to ‘deserialize’ the ConfigBuilder into a HwSpec struct. Should that fail, we map the error into a HwSpecError::Deserialize. That’s not exactly very descriptive, but the error gets handled and we’ll know that something in the TOML file does not line up with what we expect to see in a HwSpec.

Proving that this worked

In the Build command we’re seeing a last line:

println!("{}", hwspec);

This prints out the HwSpec struct that we got from the TOML file in a human-readable form. If we make it this far, we can be fully confident that this part of the configuration was ingested correctly.

Next up? The exact same trick, but for Manifest files.

Tags: