Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions pico_limbo/src/configuration/config.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::configuration::boss_bar::BossBarConfig;
use crate::configuration::compression::CompressionConfig;
use crate::configuration::env_placeholders::expand_env_placeholders;
use crate::configuration::forwarding::ForwardingConfig;
use crate::configuration::game_mode_config::GameModeConfig;
use crate::configuration::server_list::ServerListConfig;
Expand All @@ -21,6 +22,12 @@ pub enum ConfigError {

#[error("TOML deserialization error: {0}")]
TomlDeserialize(#[from] toml::de::Error),

#[error("Missing environment variable: {0}")]
MissingEnvVar(String),

#[error("Invalid environment placeholder at {line}:{char}")]
InvalidEnvPlaceholder { line: usize, char: usize },
}

/// Application configuration, serializable to/from TOML.
Expand Down Expand Up @@ -100,12 +107,13 @@ pub fn load_or_create<P: AsRef<Path>>(path: P) -> Result<Config, ConfigError> {
let path = path.as_ref();

if path.exists() {
let toml_str = fs::read_to_string(path)?;
let raw_toml_str = fs::read_to_string(path)?;

if toml_str.trim().is_empty() {
if raw_toml_str.trim().is_empty() {
create_default_config(path)
} else {
let cfg: Config = toml::from_str(&toml_str)?;
let expanded_toml_str = expand_env_placeholders(&raw_toml_str)?;
let cfg: Config = toml::from_str(expanded_toml_str.as_ref())?;
Ok(cfg)
}
} else {
Expand Down
134 changes: 134 additions & 0 deletions pico_limbo/src/configuration/env_placeholders.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
use crate::configuration::config::ConfigError;
use std::borrow::Cow;

/// Expands environment placeholders in the given text.
///
/// Replaces occurrences of `${ENV_VAR}` with the corresponding value from the
/// process environment (via `std::env`). If a referenced variable is not set,
/// returns `ConfigError::MissingEnvVar`.
///
/// The sequence `\${` is treated as an escape and is converted to a literal `${`
/// without performing substitution.
///
/// On malformed placeholders (e.g. missing closing }, empty/invalid variable
/// name, or a newline inside the placeholder), returns
/// `ConfigError::InvalidEnvPlaceholder { line, char }`, where line and char
/// are 1-based positions of the $ that started the placeholder.
///
/// For efficiency, if the input contains no `${`, the function
/// returns a `borrowed Cow::Borrowed` without allocating
pub fn expand_env_placeholders<'a>(input: &'a str) -> Result<Cow<'a, str>, ConfigError> {
if !input.contains("${") {
return Ok(Cow::Borrowed(input));
}

fn bump(ch: char, line: &mut usize, col: &mut usize) {
if ch == '\n' {
*line += 1;
*col = 1;
} else {
*col += 1;
}
}

fn is_valid_env_name(name: &str) -> bool {
let mut it = name.chars();
let Some(first) = it.next() else {
return false;
};
if !(first == '_' || first.is_ascii_alphabetic()) {
return false;
}
it.all(|c| c == '_' || c.is_ascii_alphanumeric())
}

let mut out = String::with_capacity(input.len());
let mut it = input.char_indices().peekable();

let mut line: usize = 1;
let mut col: usize = 1;

while let Some((_i, ch)) = it.next() {
let start_line = line;
let start_col = col;

match ch {
'\\' => {
let mut look = it.clone();
if matches!(look.next(), Some((_, '$'))) && matches!(look.next(), Some((_, '{'))) {
// съедаем '$' и '{'
let (_, d) = it.next().unwrap();
let (_j, b) = it.next().unwrap();

// учёт позиции
bump('\\', &mut line, &mut col);
bump(d, &mut line, &mut col);
bump(b, &mut line, &mut col);

out.push_str("${");
continue;
}

bump('\\', &mut line, &mut col);
out.push('\\');
}
'$' => {
if !matches!(it.peek(), Some((_, '{'))) {
bump('$', &mut line, &mut col);
out.push('$');
continue;
}
let (brace_idx, brace) = it.next().unwrap();

bump('$', &mut line, &mut col);
bump(brace, &mut line, &mut col);
let name_start = brace_idx + brace.len_utf8();
let mut name_end: Option<usize> = None;

while let Some((k, c)) = it.next() {
match c {
'}' => {
name_end = Some(k);
bump('}', &mut line, &mut col);
break;
}
'\n' => {
return Err(ConfigError::InvalidEnvPlaceholder {
line: start_line,
char: start_col,
});
}
_ => bump(c, &mut line, &mut col),
}
}

let name_end = name_end.ok_or(ConfigError::InvalidEnvPlaceholder {
line: start_line,
char: start_col,
})?;

let name = &input[name_start..name_end];

if name.is_empty() || !is_valid_env_name(name) {
return Err(ConfigError::InvalidEnvPlaceholder {
line: start_line,
char: start_col,
});
}

let Some(val) = std::env::var_os(name) else {
return Err(ConfigError::MissingEnvVar(name.to_string()));
};

out.push_str(&val.to_string_lossy());
}

_ => {
bump(ch, &mut line, &mut col);
out.push(ch);
}
}
}

Ok(Cow::Owned(out))
}
1 change: 1 addition & 0 deletions pico_limbo/src/configuration/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod boss_bar;
mod compression;
pub mod config;
mod env_placeholders;
mod forwarding;
mod game_mode_config;
mod require_boolean;
Expand Down
11 changes: 11 additions & 0 deletions pico_limbo/src/server/start_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@ fn load_configuration(config_path: &PathBuf) -> Option<Config> {
Err(ConfigError::Io(message, ..)) => {
error!("Failed to load configuration: {}", message);
}
Err(ConfigError::InvalidEnvPlaceholder { line, char }) => {
error!(
"Failed to load configuration: invalid environment placeholder at {line}:{char}"
);
}
Err(ConfigError::MissingEnvVar(var)) => {
error!(
"Failed to load configuration: missing environment variable {}",
var
);
}
Err(ConfigError::TomlSerialize(message, ..)) => {
error!("Failed to save default configuration file: {}", message);
}
Expand Down
68 changes: 68 additions & 0 deletions server.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
bind = "0.0.0.0:25565"
welcome_message = "Welcome to PicoLimbo!"
action_bar = "Welcome to PicoLimbo!"
default_game_mode = "spectator"
hardcore = false
fetch_player_skins = false
reduced_debug_info = false
allow_unsupported_versions = false
allow_flight = false

[forwarding]
method = "NONE"
secret = "${PATH}"

[world]
spawn_position = [
0.0,
320.0,
0.0,
]
spawn_rotation = [
0.0,
0.0,
]
dimension = "end"
time = "day"

[world.experimental]
view_distance = 2
schematic_file = ""
lock_time = false

[world.boundaries]
enabled = true
min_y = -64
teleport_message = "<red>You have reached the bottom of the world.</red>"

[server_list]
reply_to_status = true
max_players = 20
message_of_the_day = "A Minecraft Server"
show_online_player_count = true
server_icon = "server-icon.png"

[compression]
threshold = -1
level = 6

[tab_list]
enabled = true
header = "<bold>Welcome to PicoLimbo</bold>"
footer = "<green>Enjoy your stay!</green>"
player_listed = true

[boss_bar]
enabled = false
title = "<bold>Welcome to PicoLimbo!</bold>"
health = 1.0
color = "pink"
division = 0

[title]
enabled = false
title = "<bold>Welcome!</bold>"
subtitle = "Enjoy your stay"
fade_in = 10
stay = 70
fade_out = 20