Function Causing Unknown Error

I have developed a for-loop with an automatically-incrementing counter, with a TOML-based configuration-file, and defaults for that TOML-based configuration-file for when the file does not exist. Upon running my code, which was working until adding the default parameters, it attempts to create the initialisation-file twice, always failing to create it (and throwing exit-code 1, as coded into that error branch).

My project is split into 3 modules; all of which are shown, below, in their entirety.

I cannot seem to find where the issue is caused. After hours of debugging, I am hoping someone with more experience and/or knowledge can see where the issue lies. Thank you in advance to anyone taking the time to assist with this.

main.rs:

mod config;
mod generate;


fn main() {

	config::config_read();

	generate::generate();

}

config.rs:

use std::fs;
use std::sync::LazyLock;
use serde::Deserialize;


// Master struct containing the blocks of the TOML-based configuration-file.
#[derive(Debug, Default, Deserialize)]
pub struct Config {
	pub cert: Cert,
	pub key: Key,
}

// struct containing the keys of the "cert" block in the TOML-based configuration-file.
#[derive(Debug, Default, Deserialize)]
pub struct Cert {
	#[serde(default = "default_principle")]
	pub principle: String,
	#[serde(default = "default_validity")]
	pub validity: String,
}

// struct containing the keys of the "key" block in the TOML-based configuration-file.
#[derive(Debug, Default, Deserialize)]
pub struct Key {
	#[serde(default = "default_ca_priv_key")]
	pub ca_priv_key: String,
	#[serde(default = "default_user_dir")]
	pub user_dir: String,
	#[serde(default = "default_cert_serial")]
	pub cert_serial: String,
}


// Functions containing the logic for the default configuration-file values.
fn default_principle() -> String { "root".to_string() }

fn default_validity() -> String { "+7d".to_string() }

fn default_ca_priv_key() -> String {

	// Get the value of the "$HOME" environment variable.
	let home_env = std::env::var("HOME").unwrap();

	format!("{}/.ssh/id_ed25519", &home_env)

}

fn default_user_dir() -> String {

	// Get the value of the "$HOME" environment variable.
	let home_env = std::env::var("HOME").unwrap();

	format!("{}/.ssh", &home_env)

}

fn default_cert_serial() -> String {

	// Get the value of the "$HOME" environment variable.
	let home_env = std::env::var("HOME").unwrap();

	format!("{}/.ssh/cert-serial.txt", &home_env)

}


// Function containing the logic for reading the TOML-based configuration-file.
pub fn config_read() -> Config {

	// Get the value of the "$HOME" environment variable.
	let home_env = std::env::var("HOME").unwrap();
	// Append the configuration-file path to the "$HOME" environment-variable value.
	let config_file = format!("{}/.config/signatory/config.toml", home_env);
	// Parse the keys inside of the configuration-file and return the result of the parsing to the caller.
	match fs::read_to_string(&config_file) {
		Ok(content) => return toml::from_str(&content).unwrap_or_default(),
		Err(_) => {
			println!("\x1b[1;33mWARN: Configuration-file not found. Using default configuration.\x1b[0m");
			Config::default()
		}

	}

}


// static allowing the configuration-file structs to be referenced in other modules (target: generate.rs).
pub static CONFIG: LazyLock<Config> = LazyLock::new (|| {

	config_read()

});

generate.rs:

use std::fs;
use std::fs::{File, OpenOptions};
use std::io;
use std::path::Path;
use std::process::Command;
use std::process;
use glob::glob;

#[path = "config.rs"]
mod config;


// Function containing the logic for checking whether the certificate-serial-file exists.
fn cert_serial_file_init(path: &str) -> io::Result<String> {

	// If the certificate-serial-file exists, open it. If it does not exist, create it, then open it.
	if !Path::new(path).exists() {
		println!("\x1b[1;33mWARN: Certificate-serial-file does not exist. Creating certificate-serial-file...\x1b[0m");
		File::create(path)
			.map_err(|e| {
				eprintln!("\x1b[1;31mERROR (1): Failed to create certificate-serial-file!");
				process::exit(1);
			});
		println!("\x1b[1;32mSuccessfully created certificate-serial-file.\x1b[0m");

	}

	// If the certificate-serial-file contains a string, return that string. If it does not contain a string,
	// zero-initialise the string.
	let mut content = fs::read_to_string(path).unwrap_or_default();
	if content.trim().is_empty() {
		println!("\x1b[1;33mWARN: Certificate-serial-file empty. Zero-initialising certificate-serial-file...\x1b[0m");
		fs::write(path, "0")
			.map_err(|e| {
				eprintln!("\x1b[1;31mERROR (2): Failed to zero-initialise certificate-serial-file!");
				process::exit(2);
			});
		println!("\x1b[1;32mSuccessfully zero-initialised certificate-serial-file.\x1b[0m");
		content = "0".to_string()
	}
	Ok(content)

}


// Function containing the logic for stripping the path and filename-extension from OpenSSH public-keys, in order to use
// the result as the public-key principle.
fn strip_ext(filename: &str) -> &str {

	filename.rsplit('/').next().unwrap_or(filename).split('.').next().unwrap_or_else(|| {
		eprintln!("\x1b[1;31mERROR (3): Failed to strip path and filename extension from public-key!\x1b[0m");
		process::exit(3);
	})

}


// Function containing the logic for the serial-number incrementation after each OpenSSH-certificate generation.
fn increment(n: &mut u32) {

	*n += 1

}


// Function containing the logic for the OpenSSH-certificate generation.
pub fn generate() {

	// Variable containing the passed result of the configuration-file structs (for more information, consult
	// config.rs).
	let config = &*crate::config::CONFIG;

	// Variable containing the user OpenSSH public-keys, by appending the public-key filename extension(s) to the user
	// OpenSSH public-key directory.
	let mut user_dir_pubkey = [&config.key.user_dir, "/*.pub"].join("");

	// Check whether the certificate-serial-file exists and contains a string to use as the OpenSSH-certificate serial
	// number.
	cert_serial_file_init(&config.key.cert_serial);

	// Iterate through the defined user OpenSSH public-keys and generate respective OpenSSH certificates for them.
	//
	// The certificate-serial-file is read before each certificate generation, in order to use the value as the serial
	// number for the next certificate.
	for user in glob(&user_dir_pubkey).unwrap_or_else(|_| {
		eprintln!("\x1b[1;31mERROR (4): Failed to read glob pattern!\x1b[0m");
		process::exit(4);
	}) {

		// Variable containing the contents of the certificate-serial-file.
		let mut serial = fs::read_to_string(&config.key.cert_serial).expect("ERROR: Failed to parse initial serial number!");
		let mut serial_int: u32 = serial.trim().parse::<u32>().expect("ERROR: Failed to parse initial serial number!");

		let pub_key = user.unwrap_or_else(|_| {
			eprintln!("\x1b[1;31mERROR (5): Failed to read glob result!\x1b[0m");
			process::exit(5);
		});

		// Check if the user OpenSSH public-key is valid via checking whether the filename can be read, it uses UTF-8
		// character-encoding, and it is not a currently-existing OpenSSH-certificate-file.
		if pub_key
			.file_name()
			.unwrap_or_else(|| {
				eprintln!("\x1b[1;31mERROR (6): Failed to read filename!\x1b[0m");
				process::exit(6);
			})
			.to_str()
			.unwrap_or_else(|| {
				eprintln!("\x1b[1;31mERROR (7): Detected non-UTF-8 filename!\x1b[0m");
				process::exit(7);
			})
			.ends_with("-cert.pub") {
				continue
		}

		// Strip the filename extension from the public-key filename, in order to use it as the OpenSSH public-key
		// principle.
		let pub_key_str: &str = &pub_key.display().to_string();
		let pub_key_principle: &str = strip_ext(&pub_key_str);

		// Begin generating the OpenSSH certificates.
		println!("Generating OpenSSH certificate for {}...", &pub_key_principle);
		let cert_gen = Command::new("ssh-keygen")
			.args([
				// OpenSSH certificate-authority private-key.
				"-s", &config.key.ca_priv_key,
				// OpenSSH public-key principle.
				"-I", &pub_key_principle,
				// OpenSSH remote principle(s).
				"-n", &config.cert.principle,
				// OpenSSH-certificate validity timeframe.
				"-V", &["".to_string(), config.cert.validity.to_string(), "".to_string()].join(""),
				// Clear default OpenSSH-certificate permissions.
				"-O", "clear",
				// Add pty permission to OpenSSH certificate.
				"-O", "permit-pty",
				// Inject the contents of the certificate-serial-file as the certificate serial number.
				"-z", &serial_int.to_string(),
				// OpenSSH public-key file to sign.
				&pub_key.display().to_string(),
			])
			.status()
			.unwrap_or_else(|_| {
				eprintln!("\x1b[1;31mERROR (8): Failed to generate OpenSSH certificate for {}!\x1b[0m",
					&pub_key.display().to_string());
				process::exit(8);
			});
		// Convert the string read from the certificate-serial-file to an integer to allow it to be incremented as a
		// number.
		let mut serial_int: u32 = serial.trim().parse().expect("ERROR: Failed to convert serial-number string to integer!");
		increment(&mut serial_int);
		fs::write(&config.key.cert_serial, &serial_int.to_string());

	}


}

Serde's default values are applied only when you deserialize and the corresponding field is not encountered by the deserializer. They have no bearing on your impl Default for Config implementation (which you implemented using derive(Default). The default value for String is "", so the path you want to create your serial file at is "", not "$HOME/.ssh/cert-serial.txt".

I've turned your snippets into a playground by the way and added some debug output to it.

Does that mean it will apply defaults only to lines/keys which do not exist? I removed the entire configuration-file and encountered no change in the output. What is the correct way to do this other than force a configuration-file to already exist before application start?

I used the playground you made, and saw that the output shows empty strings.

You could manually implement Default for Key (and Cert as well I reckon):

impl Default for Key {
    fn default() -> Self {
        Self {
            ca_priv_key: default_ca_priv_key(),
            user_dir: default_user_dir(),
            cert_serial: default_cert_serial(),
        }
    }    
}

Playground.

Note that you shouldn't call config::config_read from main; it will be done one time by the first access to the static CONFIG.

You might want to do LazyLock::force(&config::CONFIG) though, so that if file generation fails the program panics early.
You might also want to add path.display() into your error messages to make it clear which file/path is bad.

@jofas @ProgramCrafter Implementing both of your fixes resolved the issue, and the default configuration values are used upon detecting no existing configuration-file. Adding a configuration-file overrides those defaults, as intended.

Thank you for your assistance. I appreciate it.